diff --git a/.eslintrc.js b/.eslintrc.js
index eff08d41f..04fe0f43c 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',
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 27de7a7c1..841dba687 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -23,6 +23,6 @@ packages/tests/src/plugins/injection                                  @yoannmoin
 packages/plugins/telemetry                                            @DataDog/frontend-devx @yoannmoinet
 packages/tests/src/plugins/telemetry                                  @DataDog/frontend-devx @yoannmoinet
 
-# Rum
-packages/plugins/rum                                                  @DataDog/rum @yoannmoinet
-packages/tests/src/plugins/rum                                        @DataDog/rum @yoannmoinet
+# Error Tracking
+packages/plugins/error-tracking                                       @yoannmoinet
+packages/tests/src/plugins/error-tracking                             @yoannmoinet
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 0ba48f334..85c58b66d 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -8,20 +8,95 @@ jobs:
       fail-fast: false
       matrix:
         node:
-        - 18
-        - 20
+        - node-version-file: 'package.json'
+        - node-version: 20.x
 
-    name: Unit tests w/ Node.js ${{matrix.node}}.x
+    name: Unit tests w/ Node.js ${{matrix.node.node-version || matrix.node.node-version-file}}
     runs-on: ubuntu-latest
 
     steps:
     - uses: actions/checkout@v4
-    - name: Install Node ${{matrix.node}}.x
+
+    - name: Install Node ${{matrix.node.node-version || matrix.node.node-version-file}}
       uses: actions/setup-node@v4
+      with: ${{matrix.node}}
+
+    - name: Cache build:all
+      id: cache-build
+      uses: actions/cache@v4
       with:
-        node-version: ${{matrix.node}}.x
+        path: packages/published/**/dist
+        key: ${{ matrix.node }}-cache-build-${{ hashFiles('packages/published/**', 'yarn.lock') }}
+
     - run: yarn install
-    - run: yarn test
+
+    - name: Build all plugins
+      if: steps.cache-build.outputs.cache-hit != 'true'
+      run: yarn build:all
+
+    - run: yarn test:unit
+
+  e2e:
+    timeout-minutes: 10
+
+    name: End to End
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v4
+
+    - name: Install Node
+      uses: actions/setup-node@v4
+      with:
+        node-version-file: 'package.json'
+
+    - name: Cache build:all
+      id: cache-build
+      uses: actions/cache@v4
+      with:
+        path: packages/published/**/dist
+        key: node18-cache-build-${{ hashFiles('packages/published/**', 'yarn.lock') }}
+
+    - name: Cache playwright binaries
+      id: cache-playwright-binaries
+      uses: actions/cache@v4
+      with:
+        path: |
+          ~/.cache/ms-playwright
+          ~/Library/Caches/ms-playwright
+          %USERPROFILE%\AppData\Local\ms-playwright
+        key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }}
+
+    - run: yarn install
+
+    - name: Install playwright
+      run: yarn workspace @dd/tests playwright install --with-deps
+
+    - name: Build all plugins
+      if: steps.cache-build.outputs.cache-hit != 'true'
+      run: yarn build:all
+
+    - run: yarn test:e2e
+
+    - name: Save playwright cache
+      if: always() && steps.cache-playwright-binaries.outputs.cache-hit != 'true'
+      id: save-playwright-cache
+      uses: actions/cache/save@v4
+      with:
+        path: |
+          ~/.cache/ms-playwright
+          ~/Library/Caches/ms-playwright
+          %USERPROFILE%\AppData\Local\ms-playwright
+        key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }}
+
+    - uses: actions/upload-artifact@v4
+      if: ${{ !cancelled() && failure() }}
+      with:
+        name: playwright
+        path: |
+          packages/tests/playwright-report
+          packages/tests/test-results
+        retention-days: 3
 
   lint:
     name: Linting
@@ -29,12 +104,27 @@ jobs:
 
     steps:
     - uses: actions/checkout@v4
-    - name: Install node
+
+    - name: Install Node
       uses: actions/setup-node@v4
       with:
-        node-version: '18.19.0'
+        node-version-file: 'package.json'
+
+    - name: Cache build:all
+      id: cache-build
+      uses: actions/cache@v4
+      with:
+        path: packages/published/**/dist
+        key: node18-cache-build-${{ hashFiles('packages/published/**', 'yarn.lock') }}
+
     - run: yarn install
-    - run: yarn build:all
+
+    - name: Build all plugins
+      if: steps.cache-build.outputs.cache-hit != 'true'
+      run: yarn build:all
+
     - run: yarn typecheck:all
+
     - run: yarn cli integrity
+
     - run: git diff --exit-code && git diff --cached --exit-code || (echo "Please run 'yarn cli integrity' and commit the result." && exit 1)
diff --git a/.node-version b/.node-version
deleted file mode 100644
index a9d087399..000000000
--- a/.node-version
+++ /dev/null
@@ -1 +0,0 @@
-18.19.0
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/@playwright-test-npm-1.49.1-6026636721-bb0d5eda58.zip b/.yarn/cache/@playwright-test-npm-1.49.1-6026636721-bb0d5eda58.zip
new file mode 100644
index 000000000..611afa47a
Binary files /dev/null and b/.yarn/cache/@playwright-test-npm-1.49.1-6026636721-bb0d5eda58.zip differ
diff --git a/.yarn/cache/@rollup-plugin-esm-shim-npm-0.1.7-6570f9e71a-c3cc762ce7.zip b/.yarn/cache/@rollup-plugin-esm-shim-npm-0.1.7-6570f9e71a-c3cc762ce7.zip
new file mode 100644
index 000000000..26985dd79
Binary files /dev/null and b/.yarn/cache/@rollup-plugin-esm-shim-npm-0.1.7-6570f9e71a-c3cc762ce7.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/@types-lodash-npm-4.17.14-6b38705727-6ee40725f3.zip b/.yarn/cache/@types-lodash-npm-4.17.14-6b38705727-6ee40725f3.zip
new file mode 100644
index 000000000..e1c1ed3f5
Binary files /dev/null and b/.yarn/cache/@types-lodash-npm-4.17.14-6b38705727-6ee40725f3.zip differ
diff --git a/.yarn/cache/@types-lodash.template-npm-4.5.3-a68fcd8d4e-7c9d32b8d8.zip b/.yarn/cache/@types-lodash.template-npm-4.5.3-a68fcd8d4e-7c9d32b8d8.zip
new file mode 100644
index 000000000..5c51bf9cc
Binary files /dev/null and b/.yarn/cache/@types-lodash.template-npm-4.5.3-a68fcd8d4e-7c9d32b8d8.zip differ
diff --git a/.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip b/.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip
new file mode 100644
index 000000000..816292417
Binary files /dev/null and b/.yarn/cache/fsevents-npm-2.3.2-a881d6ac9f-6b5b6f5692.zip differ
diff --git a/.yarn/cache/fsevents-patch-19706e7e35-10.zip b/.yarn/cache/fsevents-patch-19706e7e35-10.zip
new file mode 100644
index 000000000..aff1ab12c
Binary files /dev/null and b/.yarn/cache/fsevents-patch-19706e7e35-10.zip differ
diff --git a/.yarn/cache/lodash._reinterpolate-npm-3.0.0-3c62ca439e-06d2d5f331.zip b/.yarn/cache/lodash._reinterpolate-npm-3.0.0-3c62ca439e-06d2d5f331.zip
new file mode 100644
index 000000000..cf016b22b
Binary files /dev/null and b/.yarn/cache/lodash._reinterpolate-npm-3.0.0-3c62ca439e-06d2d5f331.zip differ
diff --git a/.yarn/cache/lodash.template-npm-4.5.0-5272df3039-56d18ba410.zip b/.yarn/cache/lodash.template-npm-4.5.0-5272df3039-56d18ba410.zip
new file mode 100644
index 000000000..325273d92
Binary files /dev/null and b/.yarn/cache/lodash.template-npm-4.5.0-5272df3039-56d18ba410.zip differ
diff --git a/.yarn/cache/lodash.templatesettings-npm-4.2.0-15fbdebcf4-ef470fa8b6.zip b/.yarn/cache/lodash.templatesettings-npm-4.2.0-15fbdebcf4-ef470fa8b6.zip
new file mode 100644
index 000000000..d1d09d5dd
Binary files /dev/null and b/.yarn/cache/lodash.templatesettings-npm-4.2.0-15fbdebcf4-ef470fa8b6.zip differ
diff --git a/.yarn/cache/playwright-core-npm-1.49.1-a372dbc965-baa39a5302.zip b/.yarn/cache/playwright-core-npm-1.49.1-a372dbc965-baa39a5302.zip
new file mode 100644
index 000000000..c1722107a
Binary files /dev/null and b/.yarn/cache/playwright-core-npm-1.49.1-a372dbc965-baa39a5302.zip differ
diff --git a/.yarn/cache/playwright-npm-1.49.1-0a8fed5892-49fb063f4a.zip b/.yarn/cache/playwright-npm-1.49.1-0a8fed5892-49fb063f4a.zip
new file mode 100644
index 000000000..d52f8a070
Binary files /dev/null and b/.yarn/cache/playwright-npm-1.49.1-0a8fed5892-49fb063f4a.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/LICENSES-3rdparty.csv b/LICENSES-3rdparty.csv
index 8405fc40f..fb065bf4f 100644
--- a/LICENSES-3rdparty.csv
+++ b/LICENSES-3rdparty.csv
@@ -167,10 +167,13 @@ Component,Origin,Licence,Copyright
 @nodelib/fs.walk,npm,MIT,(https://www.npmjs.com/package/@nodelib/fs.walk)
 @pkgjs/parseargs,npm,MIT,(https://github.com/pkgjs/parseargs#readme)
 @pkgr/core,npm,MIT,JounQin (https://github.com/un-ts/pkgr/blob/master/packages/core)
+@playwright/test,npm,Apache-2.0,Microsoft Corporation (https://playwright.dev)
 @rollup/plugin-babel,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/babel#readme)
 @rollup/plugin-commonjs,virtual,MIT,Rich Harris (https://github.com/rollup/plugins/tree/master/packages/commonjs/#readme)
+@rollup/plugin-esm-shim,virtual,MIT,Peter Placzek (https://github.com/rollup/plugins/tree/master/packages/esm-shim#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)
@@ -205,6 +208,8 @@ Component,Origin,Licence,Copyright
 @types/jest,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/jest)
 @types/json-schema,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema)
 @types/json5,npm,MIT,Jason Swearingen (https://www.npmjs.com/package/@types/json5)
+@types/lodash,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash)
+@types/lodash.template,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash.template)
 @types/minimatch,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/minimatch)
 @types/mute-stream,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mute-stream)
 @types/node,npm,MIT,(https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node)
@@ -640,9 +645,12 @@ listr2,npm,MIT,Cenk Kilic (https://srcs.kilic.dev)
 loader-runner,npm,MIT,Tobias Koppers @sokra (https://github.com/webpack/loader-runner#readme)
 loader-utils,npm,MIT,Tobias Koppers @sokra (https://www.npmjs.com/package/loader-utils)
 locate-path,npm,MIT,Sindre Sorhus (sindresorhus.com)
+lodash._reinterpolate,npm,MIT,John-David Dalton (https://lodash.com/)
 lodash.debounce,npm,MIT,John-David Dalton (https://lodash.com/)
 lodash.memoize,npm,MIT,John-David Dalton (https://lodash.com/)
 lodash.merge,npm,MIT,John-David Dalton (https://lodash.com/)
+lodash.template,npm,MIT,John-David Dalton (https://lodash.com/)
+lodash.templatesettings,npm,MIT,John-David Dalton (https://lodash.com/)
 log-symbols,npm,MIT,Sindre Sorhus (sindresorhus.com)
 log-update,npm,MIT,Sindre Sorhus (sindresorhus.com)
 lru-cache,npm,ISC,Isaac Z. Schlueter (https://www.npmjs.com/package/lru-cache)
@@ -739,6 +747,8 @@ picomatch,npm,MIT,Jon Schlinkert (https://github.com/micromatch/picomatch)
 pify,npm,MIT,Sindre Sorhus (sindresorhus.com)
 pirates,npm,MIT,Ari Porad (https://github.com/danez/pirates#readme)
 pkg-dir,npm,MIT,Sindre Sorhus (sindresorhus.com)
+playwright,npm,Apache-2.0,Microsoft Corporation (https://playwright.dev)
+playwright-core,npm,Apache-2.0,Microsoft Corporation (https://playwright.dev)
 please-upgrade-node,npm,MIT,typicode (https://github.com/typicode/please-upgrade-node#readme)
 posix-character-classes,npm,MIT,Jon Schlinkert (https://github.com/jonschlinkert/posix-character-classes)
 possible-typed-array-names,npm,MIT,Jordan Harband (https://github.com/ljharb/possible-typed-array-names#readme)
@@ -822,6 +832,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/README.md b/README.md
index 382fdbf6d..37754e150 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,13 @@
 # Datadog Build Plugins <!-- #omit in toc -->
 
 A set of bundler plugins for:
- - <img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" /> Webpack
- - <img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> Vite
- - <img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> esbuild
- - <img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> Rollup
- - <img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> Rspack
+<!-- #list-of-bundlers -->
+- [<img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> esbuild `@datadog/esbuild-plugin`](/packages/published/esbuild-plugin#readme)
+- [<img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> Rollup `@datadog/rollup-plugin`](/packages/published/rollup-plugin#readme)
+- [<img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> Rspack `@datadog/rspack-plugin`](/packages/published/rspack-plugin#readme)
+- [<img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> Vite `@datadog/vite-plugin`](/packages/published/vite-plugin#readme)
+- [<img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" /> Webpack `@datadog/webpack-plugin`](/packages/published/webpack-plugin#readme)
+<!-- #list-of-bundlers -->
 
 To interact with Datadog directly from your builds.
 
@@ -17,208 +19,44 @@ To interact with Datadog directly from your builds.
 <!-- This is auto generated with yarn cli integrity -->
 
 <!-- #toc -->
--   [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-----)
+    -   [Telemetry](#telemetry-----)
 -   [Contributing](#contributing)
 -   [License](#license)
 <!-- #toc -->
 
-## Bundler Plugins
-
-<!-- #list-of-bundlers -->
-### <img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> 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.
-
-
-<kbd>[📝 More details ➡️](/packages/published/esbuild-plugin#readme)</kbd>
-
-### <img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> 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.
-
-
-<kbd>[📝 More details ➡️](/packages/published/rollup-plugin#readme)</kbd>
-
-### <img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> 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.
-
-
-<kbd>[📝 More details ➡️](/packages/published/rspack-plugin#readme)</kbd>
-
-### <img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> 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.
-
-
-<kbd>[📝 More details ➡️](/packages/published/vite-plugin#readme)</kbd>
-
-### <img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" /> Webpack
-
-`@datadog/webpack-plugin`
+## Installation
 
-#### 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 +64,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.
 
-
-<kbd>[📝 More details ➡️](/packages/published/webpack-plugin#readme)</kbd>
+Follow the specific documentation for each bundler:
+<!-- #list-of-bundlers -->
+- [<img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> esbuild `@datadog/esbuild-plugin`](/packages/published/esbuild-plugin#readme)
+- [<img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> Rollup `@datadog/rollup-plugin`](/packages/published/rollup-plugin#readme)
+- [<img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> Rspack `@datadog/rspack-plugin`](/packages/published/rspack-plugin#readme)
+- [<img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> Vite `@datadog/vite-plugin`](/packages/published/vite-plugin#readme)
+- [<img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" /> Webpack `@datadog/webpack-plugin`](/packages/published/webpack-plugin#readme)
 <!-- #list-of-bundlers -->
 
-## Features
-
-<!-- #list-of-packages -->
-### RUM <img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> <img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> <img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> <img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> <img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" />
-
-> 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,
-        },
-    }
-});
-```
-
-<kbd>[📝 Full documentation ➡️](/packages/plugins/rum#readme)</kbd>
-
-### Telemetry <img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> <img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> <img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> <img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> <img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" />
-
-> 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
 
-<kbd>[📝 Full documentation ➡️](/packages/plugins/telemetry#readme)</kbd>
-<!-- #list-of-packages -->
+<details>
 
-## Configuration
+<summary>Full configuration object</summary>
 
 <!-- #full-configuration -->
 ```typescript
@@ -296,7 +89,7 @@ datadogWebpackPlugin({
     };
     customPlugins?: (options: Options, context: GlobalContext, log: Logger) => UnpluginPlugin[];
     logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'none';
-    rum?: {
+    errorTracking?: {
         disabled?: boolean;
         sourcemaps?: {
             bailOnError?: boolean;
@@ -328,12 +121,20 @@ datadogWebpackPlugin({
 ```
 <!-- #full-configuration -->
 
+</details>
+
 ### `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).
+
 ### `logLevel`
 
 > default: `'warn'`
@@ -367,7 +168,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.
+
+<details>
+
+<summary>Full context object</summary>
 
 <!-- #global-context-type -->
 ```typescript
@@ -433,16 +238,77 @@ type GlobalContext = {
     version: string;
 }
 ```
+<!-- #global-context-type -->
 
-> [!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.
+</details>
 
-<!-- #global-context-type -->
+#### [📝 Full documentation ➡️](/packages/factory#global-context)
+
+## Features
+
+<!-- #list-of-packages -->
+### Error Tracking <img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> <img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> <img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> <img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> <img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" />
+
+> Interact with Error Tracking directly from your build system.
 
-Your function will need to return an array of [Unplugin Plugins definitions](https://unplugin.unjs.io/guide/#supported-hooks).
+#### [📝 Full documentation ➡️](/packages/plugins/error-tracking#readme)
+
+<details>
+
+<summary>Configuration</summary>
+
+```typescript
+datadogWebpackPlugin({
+    errorTracking?: {
+        disabled?: boolean,
+        sourcemaps?: {
+            bailOnError?: boolean,
+            dryRun?: boolean,
+            intakeUrl?: string,
+            maxConcurrency?: number,
+            minifiedPathPrefix: string,
+            releaseVersion: string,
+            service: string,
+        },
+    }
+});
+```
+
+</details>
+
+### Telemetry <img src="packages/assets/src/esbuild.svg" alt="ESBuild" width="17" /> <img src="packages/assets/src/rollup.svg" alt="Rollup" width="17" /> <img src="packages/assets/src/rspack.svg" alt="Rspack" width="17" /> <img src="packages/assets/src/vite.svg" alt="Vite" width="17" /> <img src="packages/assets/src/webpack.svg" alt="Webpack" width="17" />
+
+> Display and send telemetry data as metrics to Datadog.
+
+#### [📝 Full documentation ➡️](/packages/plugins/telemetry#readme)
+
+<details>
+
+<summary>Configuration</summary>
+
+```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)[],
+    }
+});
+```
+
+</details>
+<!-- #list-of-packages -->
 
 ## Contributing
 
@@ -452,4 +318,4 @@ Check out [CONTRIBUTING.md](/CONTRIBUTING.md) for more information about how to
 
 [MIT](/LICENSE)
 
-<kbd>[Back to top :arrow_up:](#top)</kbd>
+### [Back to top :arrow_up:](#top) <!-- #omit in toc -->
diff --git a/package.json b/package.json
index d4bbd2602..fa5594976 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,6 @@
         "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",
         "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/constants.ts b/packages/core/src/constants.ts
index 4982a9c06..13096e63a 100644
--- a/packages/core/src/constants.ts
+++ b/packages/core/src/constants.ts
@@ -7,10 +7,11 @@ export const INJECTED_FILE = '__datadog-helper-file';
 export const ALL_BUNDLERS = ['webpack', 'vite', 'esbuild', 'rollup', 'rspack', 'rolldown', 'farm'];
 export const SUPPORTED_BUNDLERS = ['webpack', 'vite', 'esbuild', 'rollup', 'rspack'] as const;
 export const FULL_NAME_BUNDLERS = [
-    'webpack4',
-    'webpack5',
-    'vite',
     'esbuild',
     'rollup',
     'rspack',
+    'vite',
+    'webpack4',
+    'webpack5',
 ] as const;
+export const ENV_VAR_REQUESTED_BUNDLERS = 'PLAYWRIGHT_REQUESTED_BUNDLERS';
diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts
index 582719656..822c0c5be 100644
--- a/packages/core/src/helpers.ts
+++ b/packages/core/src/helpers.ts
@@ -4,12 +4,28 @@
 
 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 {
+    BuildReport,
+    Entry,
+    File,
+    GlobalContext,
+    Input,
+    Logger,
+    Output,
+    RequestOpts,
+    ResolvedEntry,
+    SerializedBuildReport,
+    SerializedEntry,
+    SerializedInput,
+    SerializedOutput,
+} from './types';
 
 // Format a duration 0h 0m 0s 0ms
 export const formatDuration = (duration: number) => {
@@ -25,19 +41,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<ResolvedEntry[]> => {
+    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 (entryPoints && 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 = <T>(opts: RequestOpts): Promise<T> => {
-    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 +124,24 @@ export const doRequest = <T>(opts: RequestOpts): Promise<T> => {
                     // 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 +255,158 @@ 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}`;
+
+// Returns an object that is safe to serialize to JSON.
+// Mostly useful for debugging and testing.
+export const serializeBuildReport = (report: BuildReport): SerializedBuildReport => {
+    // Report is an object that self reference some of its values.
+    // 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,
+        start: report.start,
+        end: report.end,
+        duration: report.duration,
+        writeDuration: report.writeDuration,
+        entries: [],
+        inputs: [],
+        outputs: [],
+    };
+
+    for (const entry of report.entries || []) {
+        const newEntry: SerializedEntry = { ...entry, inputs: [], outputs: [] };
+        if (entry.inputs) {
+            newEntry.inputs = entry.inputs.map((file: File) => file.filepath);
+        }
+        if (entry.outputs) {
+            newEntry.outputs = entry.outputs.map((file: File) => file.filepath);
+        }
+        jsonReport.entries.push(newEntry);
+    }
+
+    for (const input of report.inputs || []) {
+        const newInput: SerializedInput = { ...input, dependencies: [], dependents: [] };
+        if (input.dependencies) {
+            for (const dependency of input.dependencies) {
+                newInput.dependencies.push(dependency.filepath);
+            }
+        }
+        if (input.dependents) {
+            for (const dependent of input.dependents) {
+                newInput.dependents.push(dependent.filepath);
+            }
+        }
+        jsonReport.inputs.push(newInput);
+    }
+
+    for (const output of report.outputs || []) {
+        const newOutput: SerializedOutput = { ...output, inputs: [] };
+        if (output.inputs) {
+            newOutput.inputs = output.inputs.map((file: File) => file.filepath);
+        }
+        jsonReport.outputs.push(newOutput);
+    }
+
+    return jsonReport;
+};
+
+// Returns an object that is unserialized from serializeBuildReport().
+// 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,
+        start: report.start,
+        end: report.end,
+        duration: report.duration,
+        writeDuration: report.writeDuration,
+    };
+
+    const reportInputs = report.inputs || [];
+    const reportOutputs = report.outputs || [];
+
+    const entries: Entry[] = [];
+
+    // Prefill inputs and outputs as they are sometimes self-referencing themselves.
+    const indexedInputs: Map<string, Input> = new Map();
+    const inputs: Input[] = reportInputs.map<Input>((input) => {
+        const newInput: Input = {
+            ...input,
+            // Keep them empty for now, we'll fill them later.
+            dependencies: new Set(),
+            dependents: new Set(),
+        };
+        indexedInputs.set(input.filepath, newInput);
+        return newInput;
+    });
+
+    const indexedOutputs: Map<string, Output> = new Map();
+    const outputs: Output[] = reportOutputs.map<Output>((output) => {
+        const newOutput: Output = { ...output, inputs: [] };
+        indexedOutputs.set(output.filepath, newOutput);
+        return newOutput;
+    });
+
+    // Fill in the inputs' dependencies and dependents.
+    for (const input of reportInputs) {
+        const newInput: Input = indexedInputs.get(input.filepath)!;
+
+        // Re-assign the dependencies and dependents to the actual objects.
+        if (input.dependencies) {
+            for (const dependency of input.dependencies) {
+                const newDependency = indexedInputs.get(dependency)!;
+                newInput.dependencies.add(newDependency);
+            }
+        }
+        if (input.dependents) {
+            for (const dependent of input.dependents) {
+                const newDependent = indexedInputs.get(dependent)!;
+                newInput.dependents.add(newDependent);
+            }
+        }
+    }
+
+    // Fill in the outputs' inputs.
+    for (const output of reportOutputs) {
+        const newOutput: Output = indexedOutputs.get(output.filepath)!;
+        if (output.inputs) {
+            // Re-assign the inputs to the actual objects.
+            newOutput.inputs = output.inputs
+                .map<
+                    // Can be either an input or an output (for sourcemaps).
+                    Input | Output | undefined
+                >((filepath: string) => indexedInputs.get(filepath) || indexedOutputs.get(filepath))
+                .filter(Boolean) as (Input | Output)[];
+        }
+    }
+
+    for (const entry of report.entries || []) {
+        const newEntry: Entry = { ...entry, inputs: [], outputs: [] };
+        if (entry.inputs) {
+            newEntry.inputs = entry.inputs
+                .map((filepath: string) => indexedInputs.get(filepath))
+                .filter(Boolean) as (Output | Input)[];
+        }
+        if (entry.outputs) {
+            newEntry.outputs = entry.outputs
+                .map((filepath: string) => indexedOutputs.get(filepath))
+                .filter(Boolean) as Output[];
+        }
+        entries.push(newEntry);
+    }
+
+    return {
+        ...buildReport,
+        entries,
+        inputs,
+        outputs,
+    };
+};
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 11e6a6b46..a9d148a25 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -9,8 +9,8 @@
 import type { TrackedFilesMatcher } from '@dd/internal-git-plugin/trackedFilesMatcher';
 /* eslint-disable arca/import-ordering */
 // #imports-injection-marker
-import type { RumOptions } from '@dd/rum-plugin/types';
-import type * as rum from '@dd/rum-plugin';
+import type { ErrorTrackingOptions } from '@dd/error-tracking-plugin/types';
+import type * as errorTracking from '@dd/error-tracking-plugin';
 import type { TelemetryOptions } from '@dd/telemetry-plugin/types';
 import type * as telemetry from '@dd/telemetry-plugin';
 // #imports-injection-marker
@@ -41,9 +41,16 @@ export type SerializedInput = Assign<Input, { dependencies: string[]; dependents
 export type SerializedOutput = Assign<Output, { inputs: string[] }>;
 
 export type BuildReport = {
+    bundler: Omit<BundlerReport, 'outDir' | 'rawConfig'>;
     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 +81,18 @@ export type BundlerReport = {
     version: string;
 };
 
-export type ToInjectItem = { type: 'file' | 'code'; value: string; fallback?: ToInjectItem };
+export type InjectedValue = string | (() => Promise<string>);
+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 +134,7 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
 
 export type AuthOptions = {
     apiKey?: string;
+    appKey?: string;
 };
 
 export interface BaseOptions {
@@ -127,7 +146,7 @@ export interface BaseOptions {
 export interface Options extends BaseOptions {
     // Each product should have a unique entry.
     // #types-injection-marker
-    [rum.CONFIG_KEY]?: RumOptions;
+    [errorTracking.CONFIG_KEY]?: ErrorTrackingOptions;
     [telemetry.CONFIG_KEY]?: TelemetryOptions;
     // #types-injection-marker
     customPlugins?: GetCustomPlugins;
@@ -138,11 +157,14 @@ export type OptionsWithDefaults = Assign<Options, GetPluginsOptions>;
 
 export type PluginName = `datadog-${Lowercase<string>}-plugin`;
 
-type Data = { data: BodyInit; headers?: Record<string, string> };
+type Data = { data?: BodyInit; headers?: Record<string, string> };
 export type RequestOpts = {
     url: string;
+    auth?: AuthOptions;
     method?: string;
     getData?: () => Promise<Data> | 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.
 
-<kbd>[📝 Full documentation ➡️](/packages/plugins/build-report#readme)</kbd>
+#### [📝 Full documentation ➡️](/packages/plugins/build-report#readme)
+
 
 ### Bundler Report
 
 > A very basic report on the currently used bundler.<br/>
 > It is useful to unify some configurations.
 
-<kbd>[📝 Full documentation ➡️](/packages/plugins/bundler-report#readme)</kbd>
+#### [📝 Full documentation ➡️](/packages/plugins/bundler-report#readme)
+
 
 ### Git
 
 > Adds repository data to the global context from the `buildStart` hook.
 
-<kbd>[📝 Full documentation ➡️](/packages/plugins/git#readme)</kbd>
+#### [📝 Full documentation ➡️](/packages/plugins/git#readme)
+
 
 ### Injection
 
-> This is used to prepend some code to the produced bundle.<br/>
-> 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.<br/>
+> Particularly useful :
+> - to share some global context.
+> - to automatically inject some SDK.
+> - to initialise some global dependencies.
+> - ...
+
+#### [📝 Full documentation ➡️](/packages/plugins/injection#readme)
 
-<kbd>[📝 Full documentation ➡️](/packages/plugins/injection#readme)</kbd>
 <!-- #internal-plugins-list -->
 
 ## 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..b686e4eab 100644
--- a/packages/factory/package.json
+++ b/packages/factory/package.json
@@ -17,11 +17,11 @@
     },
     "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:*",
         "@dd/internal-injection-plugin": "workspace:*",
-        "@dd/rum-plugin": "workspace:*",
         "@dd/telemetry-plugin": "workspace:*",
         "chalk": "2.3.1",
         "unplugin": "1.16.0"
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<LogLevel, number> = {
 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<string, ToInjectItem>;
     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..bf633c12a 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,13 +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 { OptionsWithRum } from '@dd/rum-plugin/types';
-import * as rum from '@dd/rum-plugin';
+import type { OptionsWithErrorTracking } from '@dd/error-tracking-plugin/types';
+import * as errorTracking from '@dd/error-tracking-plugin';
 import type { OptionsWithTelemetry } from '@dd/telemetry-plugin/types';
 import * as telemetry from '@dd/telemetry-plugin';
 import { getBuildReportPlugins } from '@dd/internal-build-report-plugin';
@@ -37,10 +38,9 @@ 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 RumTypes } from '@dd/rum-plugin';
+export type { types as ErrorTrackingTypes } from '@dd/error-tracking-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 +68,7 @@ export const buildPluginFactory = ({
         }
 
         // Create the global context.
-        const injections: ToInjectItem[] = [];
+        const injections: Map<string, ToInjectItem> = new Map();
         const context: GlobalContext = getContext({
             options,
             bundlerVersion: bundler.version || bundler.VERSION,
@@ -91,6 +91,7 @@ export const buildPluginFactory = ({
             ...getGitPlugins(options, context),
             ...getInjectionPlugins(
                 bundler,
+                options,
                 context,
                 injections,
                 getLogger('datadog-injection-plugin'),
@@ -110,9 +111,16 @@ export const buildPluginFactory = ({
 
         // Based on configuration add corresponding plugin.
         // #configs-injection-marker
-        if (options[rum.CONFIG_KEY] && options[rum.CONFIG_KEY].disabled !== true) {
+        if (
+            options[errorTracking.CONFIG_KEY] &&
+            options[errorTracking.CONFIG_KEY].disabled !== true
+        ) {
             plugins.push(
-                ...rum.getPlugins(options as OptionsWithRum, context, getLogger(rum.PLUGIN_NAME)),
+                ...errorTracking.getPlugins(
+                    options as OptionsWithErrorTracking,
+                    context,
+                    getLogger(errorTracking.PLUGIN_NAME),
+                ),
             );
         }
         if (options[telemetry.CONFIG_KEY] && options[telemetry.CONFIG_KEY].disabled !== true) {
@@ -129,6 +137,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 = <T>(obj: Record<string, T>, 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<string, string> | { in: string; out: string }[] | undefined,
-    context: GlobalContext,
-): Map<string, string> => {
-    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..aeaee5dcb 100644
--- a/packages/plugins/build-report/src/helpers.ts
+++ b/packages/plugins/build-report/src/helpers.ts
@@ -4,18 +4,7 @@
 
 import { INJECTED_FILE } from '@dd/core/constants';
 import { isInjectionFile } from '@dd/core/helpers';
-import type {
-    BuildReport,
-    SerializedEntry,
-    File,
-    GlobalContext,
-    SerializedInput,
-    SerializedBuildReport,
-    SerializedOutput,
-    Entry,
-    Input,
-    Output,
-} from '@dd/core/types';
+import type { GlobalContext } from '@dd/core/types';
 import path from 'path';
 
 // Will match any last part of a path after a dot or slash and is a word character.
@@ -43,156 +32,6 @@ export const getType = (name: string): string => {
     return getExtension(cleanPath(name)) || 'unknown';
 };
 
-// Returns an object that is safe to serialize to JSON.
-// Mostly useful for debugging and testing.
-export const serializeBuildReport = (report: BuildReport): SerializedBuildReport => {
-    // Report is an object that self reference some of its values.
-    // 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 = {
-        errors: report.errors,
-        warnings: report.warnings,
-        logs: report.logs,
-        start: report.start,
-        end: report.end,
-        duration: report.duration,
-        writeDuration: report.writeDuration,
-        entries: [],
-        inputs: [],
-        outputs: [],
-    };
-
-    for (const entry of report.entries || []) {
-        const newEntry: SerializedEntry = { ...entry, inputs: [], outputs: [] };
-        if (entry.inputs) {
-            newEntry.inputs = entry.inputs.map((file: File) => file.filepath);
-        }
-        if (entry.outputs) {
-            newEntry.outputs = entry.outputs.map((file: File) => file.filepath);
-        }
-        jsonReport.entries.push(newEntry);
-    }
-
-    for (const input of report.inputs || []) {
-        const newInput: SerializedInput = { ...input, dependencies: [], dependents: [] };
-        if (input.dependencies) {
-            for (const dependency of input.dependencies) {
-                newInput.dependencies.push(dependency.filepath);
-            }
-        }
-        if (input.dependents) {
-            for (const dependent of input.dependents) {
-                newInput.dependents.push(dependent.filepath);
-            }
-        }
-        jsonReport.inputs.push(newInput);
-    }
-
-    for (const output of report.outputs || []) {
-        const newOutput: SerializedOutput = { ...output, inputs: [] };
-        if (output.inputs) {
-            newOutput.inputs = output.inputs.map((file: File) => file.filepath);
-        }
-        jsonReport.outputs.push(newOutput);
-    }
-
-    return jsonReport;
-};
-
-// Returns an object that is unserialized from serializeBuildReport().
-// Mostly useful for debugging and testing.
-export const unserializeBuildReport = (report: SerializedBuildReport): BuildReport => {
-    const buildReport: BuildReport = {
-        errors: report.errors,
-        warnings: report.warnings,
-        logs: report.logs,
-        start: report.start,
-        end: report.end,
-        duration: report.duration,
-        writeDuration: report.writeDuration,
-    };
-
-    const reportInputs = report.inputs || [];
-    const reportOutputs = report.outputs || [];
-
-    const entries: Entry[] = [];
-
-    // Prefill inputs and outputs as they are sometimes self-referencing themselves.
-    const indexedInputs: Map<string, Input> = new Map();
-    const inputs: Input[] = reportInputs.map<Input>((input) => {
-        const newInput: Input = {
-            ...input,
-            // Keep them empty for now, we'll fill them later.
-            dependencies: new Set(),
-            dependents: new Set(),
-        };
-        indexedInputs.set(input.filepath, newInput);
-        return newInput;
-    });
-
-    const indexedOutputs: Map<string, Output> = new Map();
-    const outputs: Output[] = reportOutputs.map<Output>((output) => {
-        const newOutput: Output = { ...output, inputs: [] };
-        indexedOutputs.set(output.filepath, newOutput);
-        return newOutput;
-    });
-
-    // Fill in the inputs' dependencies and dependents.
-    for (const input of reportInputs) {
-        const newInput: Input = indexedInputs.get(input.filepath)!;
-
-        // Re-assign the dependencies and dependents to the actual objects.
-        if (input.dependencies) {
-            for (const dependency of input.dependencies) {
-                const newDependency = indexedInputs.get(dependency)!;
-                newInput.dependencies.add(newDependency);
-            }
-        }
-        if (input.dependents) {
-            for (const dependent of input.dependents) {
-                const newDependent = indexedInputs.get(dependent)!;
-                newInput.dependents.add(newDependent);
-            }
-        }
-    }
-
-    // Fill in the outputs' inputs.
-    for (const output of reportOutputs) {
-        const newOutput: Output = indexedOutputs.get(output.filepath)!;
-        if (output.inputs) {
-            // Re-assign the inputs to the actual objects.
-            newOutput.inputs = output.inputs
-                .map<
-                    // Can be either an input or an output (for sourcemaps).
-                    Input | Output | undefined
-                >((filepath: string) => indexedInputs.get(filepath) || indexedOutputs.get(filepath))
-                .filter(Boolean) as (Input | Output)[];
-        }
-    }
-
-    for (const entry of report.entries || []) {
-        const newEntry: Entry = { ...entry, inputs: [], outputs: [] };
-        if (entry.inputs) {
-            newEntry.inputs = entry.inputs
-                .map((filepath: string) => indexedInputs.get(filepath))
-                .filter(Boolean) as (Output | Input)[];
-        }
-        if (entry.outputs) {
-            newEntry.outputs = entry.outputs
-                .map((filepath: string) => indexedOutputs.get(filepath))
-                .filter(Boolean) as Output[];
-        }
-        entries.push(newEntry);
-    }
-
-    return {
-        ...buildReport,
-        entries,
-        inputs,
-        outputs,
-    };
-};
-
 const BUNDLER_SPECIFICS = ['unknown', 'commonjsHelpers.js', 'vite/preload-helper.js'];
 // Make list of paths unique, remove the current file and particularities.
 export const cleanReport = <T = string>(
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..04f34f7d4 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<string> = 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: {
+            ...rollupPlugin(),
+            config(config) {
+                if (config.root) {
+                    context.cwd = config.root;
+                } else {
+                    handleCwd(Array.from(directories), context);
+                }
+            },
+        },
+        rollup: rollupPlugin(),
     };
 
     return [bundlerReportPlugin];
diff --git a/packages/plugins/rum/README.md b/packages/plugins/error-tracking/README.md
similarity index 67%
rename from packages/plugins/rum/README.md
rename to packages/plugins/error-tracking/README.md
index 601b50102..756fdf881 100644
--- a/packages/plugins/rum/README.md
+++ b/packages/plugins/error-tracking/README.md
@@ -1,6 +1,6 @@
-# RUM Plugin <!-- #omit in toc -->
+# Error Tracking Plugin <!-- #omit in toc -->
 
-Interact with our Real User Monitoring product (RUM) in Datadog directly from your build system.
+Interact with Error Tracking directly from your build system.
 
 <!-- The title and the following line will both be added to the root README.md  -->
 
@@ -11,19 +11,19 @@ Interact with our Real User Monitoring product (RUM) in Datadog directly from yo
 <!-- #toc -->
 -   [Configuration](#configuration)
 -   [Sourcemaps Upload](#sourcemaps-upload)
-    -   [`rum.sourcemaps.bailOnError`](#rumsourcemapsbailonerror)
-    -   [`rum.sourcemaps.dryRun`](#rumsourcemapsdryrun)
-    -   [`rum.sourcemaps.intakeUrl`](#rumsourcemapsintakeurl)
-    -   [`rum.sourcemaps.maxConcurrency`](#rumsourcemapsmaxconcurrency)
-    -   [`rum.sourcemaps.minifiedPathPrefix`](#rumsourcemapsminifiedpathprefix)
-    -   [`rum.sourcemaps.releaseVersion`](#rumsourcemapsreleaseversion)
-    -   [`rum.sourcemaps.service`](#rumsourcemapsservice)
+    -   [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)
 <!-- #toc -->
 
 ## Configuration
 
 ```ts
-rum?: {
+errorTracking?: {
     disabled?: boolean;
     sourcemaps?: {
         bailOnError?: boolean;
@@ -45,31 +45,31 @@ Upload JavaScript sourcemaps to Datadog to un-minify your errors.
 > 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`).
 
-### `rum.sourcemaps.bailOnError`
+### errorTracking.sourcemaps.bailOnError
 
 > default: `false`
 
 Should the upload of sourcemaps fail the build on first error?
 
-### `rum.sourcemaps.dryRun`
+### errorTracking.sourcemaps.dryRun
 
 > default: `false`
 
 It will not upload the sourcemaps to Datadog, but will do everything else.
 
-### `rum.sourcemaps.intakeUrl`
+### errorTracking.sourcemaps.intakeUrl
 
 > default: `https://sourcemap-intake.datadoghq.com/api/v2/srcmap`
 
 Against which endpoint do you want to upload the sourcemaps.
 
-### `rum.sourcemaps.maxConcurrency`
+### errorTracking.sourcemaps.maxConcurrency
 
 > default: `20`
 
 Number of concurrent upload to the API.
 
-### `rum.sourcemaps.minifiedPathPrefix`
+### errorTracking.sourcemaps.minifiedPathPrefix
 
 > required
 
@@ -79,13 +79,13 @@ 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.
 
-### `rum.sourcemaps.releaseVersion`
+### errorTracking.sourcemaps.releaseVersion
 
 > required
 
 Is similar and will be used to match the `version` tag set on the RUM SDK.
 
-### `rum.sourcemaps.service`
+### errorTracking.sourcemaps.service
 
 > required
 
diff --git a/packages/plugins/rum/package.json b/packages/plugins/error-tracking/package.json
similarity index 73%
rename from packages/plugins/rum/package.json
rename to packages/plugins/error-tracking/package.json
index 031685db4..2c19e7e22 100644
--- a/packages/plugins/rum/package.json
+++ b/packages/plugins/error-tracking/package.json
@@ -1,15 +1,15 @@
 {
-    "name": "@dd/rum-plugin",
+    "name": "@dd/error-tracking-plugin",
     "packageManager": "yarn@4.0.2",
     "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/plugins/rum#readme",
+    "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/rum"
+        "directory": "packages/plugins/error-tracking"
     },
     "exports": {
         ".": "./src/index.ts",
diff --git a/packages/plugins/rum/src/constants.ts b/packages/plugins/error-tracking/src/constants.ts
similarity index 67%
rename from packages/plugins/rum/src/constants.ts
rename to packages/plugins/error-tracking/src/constants.ts
index a27b7a174..2dde88b79 100644
--- a/packages/plugins/rum/src/constants.ts
+++ b/packages/plugins/error-tracking/src/constants.ts
@@ -4,5 +4,5 @@
 
 import type { PluginName } from '@dd/core/types';
 
-export const CONFIG_KEY = 'rum' as const;
-export const PLUGIN_NAME: PluginName = 'datadog-rum-plugin' as const;
+export const CONFIG_KEY = 'errorTracking' as const;
+export const PLUGIN_NAME: PluginName = 'datadog-error-tracking-plugin' as const;
diff --git a/packages/plugins/rum/src/index.ts b/packages/plugins/error-tracking/src/index.ts
similarity index 55%
rename from packages/plugins/rum/src/index.ts
rename to packages/plugins/error-tracking/src/index.ts
index 6cb5ddd7b..e2604e16f 100644
--- a/packages/plugins/rum/src/index.ts
+++ b/packages/plugins/error-tracking/src/index.ts
@@ -4,37 +4,46 @@
 
 import type { GlobalContext, GetPlugins, Logger } from '@dd/core/types';
 
+import { PLUGIN_NAME } from './constants';
 import { uploadSourcemaps } from './sourcemaps';
-import type { OptionsWithRum, RumOptions, RumOptionsWithSourcemaps } from './types';
+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.
-    RumOptions: RumOptions;
-    OptionsWithRum: OptionsWithRum;
+    ErrorTrackingOptions: ErrorTrackingOptions;
+    OptionsWithErrorTracking: OptionsWithErrorTracking;
 };
 
-export const getPlugins: GetPlugins<OptionsWithRum> = (
-    opts: OptionsWithRum,
+export const getPlugins: GetPlugins<OptionsWithErrorTracking> = (
+    opts: OptionsWithErrorTracking,
     context: GlobalContext,
     log: Logger,
 ) => {
     // Verify configuration.
-    const rumOptions = validateOptions(opts, log);
+    const options = validateOptions(opts, log);
     return [
         {
-            name: 'datadog-rum-sourcemaps-plugin',
+            name: PLUGIN_NAME,
             enforce: 'post',
             async writeBundle() {
-                if (rumOptions.disabled) {
+                if (options.disabled) {
                     return;
                 }
 
-                if (rumOptions.sourcemaps) {
+                if (options.sourcemaps) {
                     // Need the "as" because Typescript doesn't understand that we've already checked for sourcemaps.
-                    await uploadSourcemaps(rumOptions as RumOptionsWithSourcemaps, context, log);
+                    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<Sourcemap, 'minifiedFilePath' | 'minifiedUrl' | 'relativePath'>;
 
 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/rum/src/types.ts b/packages/plugins/error-tracking/src/types.ts
similarity index 64%
rename from packages/plugins/rum/src/types.ts
rename to packages/plugins/error-tracking/src/types.ts
index ac70fac15..3319b5740 100644
--- a/packages/plugins/rum/src/types.ts
+++ b/packages/plugins/error-tracking/src/types.ts
@@ -8,7 +8,7 @@ import type { CONFIG_KEY } from './constants';
 
 export type MinifiedPathPrefix = `http://${string}` | `https://${string}` | `/${string}`;
 
-export type RumSourcemapsOptions = {
+export type SourcemapsOptions = {
     bailOnError?: boolean;
     dryRun?: boolean;
     intakeUrl?: string;
@@ -18,25 +18,25 @@ export type RumSourcemapsOptions = {
     service: string;
 };
 
-export type RumOptions = {
+export type SourcemapsOptionsWithDefaults = Required<SourcemapsOptions>;
+
+export type ErrorTrackingOptions = {
     disabled?: boolean;
-    sourcemaps?: RumSourcemapsOptions;
+    sourcemaps?: SourcemapsOptions;
 };
 
-export type RumSourcemapsOptionsWithDefaults = Required<RumSourcemapsOptions>;
-
-export type RumOptionsWithDefaults = {
+export type ErrorTrackingOptionsWithDefaults = {
     disabled?: boolean;
-    sourcemaps?: RumSourcemapsOptionsWithDefaults;
+    sourcemaps?: SourcemapsOptionsWithDefaults;
 };
 
-export type RumOptionsWithSourcemaps = {
+export type ErrorTrackingOptionsWithSourcemaps = {
     disabled?: boolean;
-    sourcemaps: RumSourcemapsOptionsWithDefaults;
+    sourcemaps: SourcemapsOptionsWithDefaults;
 };
 
-export interface OptionsWithRum extends GetPluginsOptions {
-    [CONFIG_KEY]: RumOptions;
+export interface OptionsWithErrorTracking extends GetPluginsOptions {
+    [CONFIG_KEY]: ErrorTrackingOptions;
 }
 
 export type Sourcemap = {
diff --git a/packages/plugins/rum/src/validate.ts b/packages/plugins/error-tracking/src/validate.ts
similarity index 84%
rename from packages/plugins/rum/src/validate.ts
rename to packages/plugins/error-tracking/src/validate.ts
index 89448ab92..ec7061502 100644
--- a/packages/plugins/rum/src/validate.ts
+++ b/packages/plugins/error-tracking/src/validate.ts
@@ -7,19 +7,19 @@ import chalk from 'chalk';
 
 import { CONFIG_KEY, PLUGIN_NAME } from './constants';
 import type {
-    OptionsWithRum,
-    RumOptions,
-    RumOptionsWithDefaults,
-    RumSourcemapsOptionsWithDefaults,
+    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<OptionsWithRum>,
+    config: Partial<OptionsWithErrorTracking>,
     log: Logger,
-): RumOptionsWithDefaults => {
+): ErrorTrackingOptionsWithDefaults => {
     const errors: string[] = [];
 
     // Validate and add defaults sub-options.
@@ -33,7 +33,7 @@ export const validateOptions = (
     }
 
     // Build the final configuration.
-    const toReturn: RumOptionsWithDefaults = {
+    const toReturn: ErrorTrackingOptionsWithDefaults = {
         ...config[CONFIG_KEY],
         sourcemaps: undefined,
     };
@@ -68,11 +68,11 @@ const validateMinifiedPathPrefix = (minifiedPathPrefix: string): boolean => {
 };
 
 export const validateSourcemapsOptions = (
-    config: Partial<OptionsWithRum>,
-): ToReturn<RumOptionsWithDefaults['sourcemaps']> => {
+    config: Partial<OptionsWithErrorTracking>,
+): ToReturn<SourcemapsOptionsWithDefaults> => {
     const red = chalk.bold.red;
-    const validatedOptions: RumOptions = config[CONFIG_KEY] || {};
-    const toReturn: ToReturn<Required<RumSourcemapsOptionsWithDefaults>> = {
+    const validatedOptions: ErrorTrackingOptions = config[CONFIG_KEY] || {};
+    const toReturn: ToReturn<SourcemapsOptionsWithDefaults> = {
         errors: [],
     };
 
@@ -98,7 +98,7 @@ export const validateSourcemapsOptions = (
         }
 
         // Add the defaults.
-        const sourcemapsWithDefaults: RumSourcemapsOptionsWithDefaults = {
+        const sourcemapsWithDefaults: SourcemapsOptionsWithDefaults = {
             bailOnError: false,
             dryRun: false,
             maxConcurrency: 20,
diff --git a/packages/plugins/rum/tsconfig.json b/packages/plugins/error-tracking/tsconfig.json
similarity index 100%
rename from packages/plugins/rum/tsconfig.json
rename to packages/plugins/error-tracking/tsconfig.json
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 <!-- #omit in toc -->
 
-This is used to prepend some code to the produced bundle.<br/>
-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.<br/>
+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,<br/>
-so you'll have to have submitted your injection prior to that.<br/>
+so you'll have to "submit" your injection(s) prior to that.<br/>
 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.<br/>
@@ -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<string> => {
+    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<string> => {
     let timeoutId: ReturnType<typeof setTimeout> | undefined;
     return Promise.race([
-        doRequest<string>({ url: item.value }).finally(() => {
+        doRequest<string>({ url }).finally(() => {
             if (timeout) {
                 clearTimeout(timeoutId);
             }
@@ -30,32 +40,36 @@ export const processDistantFile = async (
     ]);
 };
 
-export const processLocalFile = async (item: ToInjectItem): Promise<string> => {
-    const absolutePath = getAbsolutePath(process.cwd(), item.value);
+export const processLocalFile = async (
+    filepath: string,
+    cwd: string = process.cwd(),
+): Promise<string> => {
+    const absolutePath = getAbsolutePath(cwd, filepath);
     return readFile(absolutePath, { encoding: 'utf-8' });
 };
 
-export const processRawCode = async (item: ToInjectItem): Promise<string> => {
-    // TODO: Confirm the code actually executes without errors.
-    return item.value;
-};
-
-export const processItem = async (item: ToInjectItem, log: Logger): Promise<string> => {
+export const processItem = async (
+    item: ToInjectItem,
+    log: Logger,
+    cwd: string = process.cwd(),
+): Promise<string> => {
     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<stri
 };
 
 export const processInjections = async (
-    toInject: ToInjectItem[],
+    toInject: Map<string, ToInjectItem>,
     log: Logger,
-): Promise<string[]> => {
-    const proms: (Promise<string> | string)[] = [];
+    cwd: string = process.cwd(),
+): Promise<Map<string, { position: InjectPosition; value: string }>> => {
+    const toReturn: Map<string, { position: InjectPosition; value: string }> = new Map();
 
-    for (const item of toInject) {
-        proms.push(processItem(item, log));
+    // Processing sequentially all the items.
+    for (const [id, item] of toInject.entries()) {
+        // eslint-disable-next-line no-await-in-loop
+        const value = await processItem(item, log, cwd);
+        if (value) {
+            toReturn.set(id, { value, position: item.position || InjectPosition.BEFORE });
+        }
     }
 
-    const results = await Promise.all(proms);
-    return results.filter(Boolean);
+    return toReturn;
+};
+
+export const getContentToInject = (contentToInject: Map<string, string>) => {
+    if (contentToInject.size === 0) {
+        return '';
+    }
+
+    const stringToInject = Array.from(contentToInject.values())
+        // Wrapping it in order to avoid variable name collisions.
+        .map((content) => `(() => {${content}})();`)
+        .join('\n\n');
+    return `${BEFORE_INJECTION}\n${stringToInject}\n${AFTER_INJECTION}`;
+};
+
+// Prepare and fetch the content to inject.
+export const addInjections = async (
+    log: Logger,
+    toInject: Map<string, ToInjectItem>,
+    contentsToInject: ContentsToInject,
+    cwd: string = process.cwd(),
+) => {
+    const results = await processInjections(toInject, log, cwd);
+    // Redistribute the content to inject in the right place.
+    for (const [id, value] of results.entries()) {
+        contentsToInject[value.position].set(id, value.value);
+    }
 };
diff --git a/packages/plugins/injection/src/index.ts b/packages/plugins/injection/src/index.ts
index f936b318c..5215a0637 100644
--- a/packages/plugins/injection/src/index.ts
+++ b/packages/plugins/injection/src/index.ts
@@ -2,197 +2,87 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { INJECTED_FILE } from '@dd/core/constants';
-import { outputFile, rm } from '@dd/core/helpers';
-import type { GlobalContext, Logger, PluginOptions, ToInjectItem } from '@dd/core/types';
-import fs from 'fs';
-import path from 'path';
-
-import { PLUGIN_NAME, PREPARATION_PLUGIN_NAME } from './constants';
-import { processInjections } from './helpers';
+import { isInjectionFile } from '@dd/core/helpers';
+import {
+    InjectPosition,
+    type GlobalContext,
+    type Logger,
+    type Options,
+    type PluginOptions,
+    type ToInjectItem,
+} from '@dd/core/types';
+
+import { PLUGIN_NAME } from './constants';
+import { getEsbuildPlugin } from './esbuild';
+import { addInjections, getContentToInject } from './helpers';
+import { getRollupPlugin } from './rollup';
+import type { ContentsToInject } from './types';
+import { getXpackPlugin } from './xpack';
 
 export { PLUGIN_NAME } from './constants';
 
 export const getInjectionPlugins = (
     bundler: any,
+    options: Options,
     context: GlobalContext,
-    toInject: ToInjectItem[],
+    toInject: Map<string, ToInjectItem>,
     log: Logger,
 ): PluginOptions[] => {
-    const contentToInject: string[] = [];
-
-    const getContentToInject = () => {
-        // Needs a non empty string otherwise ESBuild will throw 'Do not know how to load path'.
-        // Most likely because it tries to generate an empty file.
-        const before = `
-/********************************************/
-/* BEGIN INJECTION BY DATADOG BUILD PLUGINS */`;
-        const after = `
-/*  END INJECTION BY DATADOG BUILD PLUGINS  */
-/********************************************/`;
-
-        return `${before}\n${contentToInject.join('\n\n')}\n${after}`;
+    // Storage for all the positional contents we want to inject.
+    const contentsToInject: ContentsToInject = {
+        [InjectPosition.BEFORE]: new Map(),
+        [InjectPosition.MIDDLE]: new Map(),
+        [InjectPosition.AFTER]: new Map(),
     };
 
-    // Rollup uses its own banner hook.
-    // We use its native functionality.
-    const rollupInjectionPlugin: PluginOptions['rollup'] = {
-        banner(chunk) {
-            if (chunk.isEntry) {
-                return getContentToInject();
-            }
-            return '';
-        },
-    };
-
-    // Create a unique filename to avoid conflicts.
-    const INJECTED_FILE_PATH = `${Date.now()}.${performance.now()}.${INJECTED_FILE}.js`;
-
-    // This plugin happens in 2 steps in order to cover all bundlers:
-    //   1. Prepare the content to inject, fetching distant/local files and anything necessary.
-    //       a. [esbuild] We also create the actual file for esbuild to avoid any resolution errors
-    //            and keep the inject override safe.
-    //       b. [esbuild] With a custom resolver, every client side sub-builds would fail to resolve
-    //            the file when re-using the same config as the parent build (with the inject).
-    //   2. Inject a virtual file into the bundling, this file will be home of all injected content.
     const plugins: PluginOptions[] = [
-        // Prepare and fetch the content to inject for all bundlers.
         {
-            name: PREPARATION_PLUGIN_NAME,
-            enforce: 'pre',
-            // We use buildStart as it is the first async hook.
+            name: PLUGIN_NAME,
+            enforce: 'post',
+            // Bundler specific part of the plugin.
+            // We use it to:
+            // - Inject the content in the right places, each bundler offers this differently.
+            esbuild: getEsbuildPlugin(log, context, contentsToInject),
+            webpack: getXpackPlugin(bundler, log, context, toInject, contentsToInject),
+            rspack: getXpackPlugin(bundler, log, context, toInject, contentsToInject),
+            rollup: getRollupPlugin(contentsToInject),
+            vite: { ...getRollupPlugin(contentsToInject), enforce: 'pre' },
+            // Universal part of the plugin.
+            // We use it to:
+            // - Prepare the injections.
+            // - Handle the resolution of the injection file.
             async buildStart() {
-                const results = await processInjections(toInject, log);
-                contentToInject.push(...results);
-
-                // Only esbuild needs the following.
-                if (context.bundler.name !== 'esbuild') {
+                // In xpack, we need to prepare the injections before the build starts.
+                // So we do it in their specific plugin.
+                if (['webpack', 'rspack'].includes(context.bundler.name)) {
                     return;
                 }
 
-                // We put it in the outDir to avoid impacting any other part of the build.
-                // While still being under esbuild's cwd.
-                const absolutePathInjectFile = path.resolve(
-                    context.bundler.outDir,
-                    INJECTED_FILE_PATH,
-                );
-
-                // Actually create the file to avoid any resolution errors.
-                // It needs to be within cwd.
-                try {
-                    // Verify that the file doesn't already exist.
-                    if (fs.existsSync(absolutePathInjectFile)) {
-                        log.warn(`Temporary file "${INJECTED_FILE_PATH}" already exists.`);
-                    }
-                    await outputFile(absolutePathInjectFile, getContentToInject());
-                } catch (e: any) {
-                    log.error(`Could not create the file: ${e.message}`);
-                }
+                // Prepare the injections.
+                await addInjections(log, toInject, contentsToInject, context.cwd);
             },
-
-            async buildEnd() {
-                // Only esbuild needs the following.
-                if (context.bundler.name !== 'esbuild') {
-                    return;
+            async resolveId(source) {
+                if (isInjectionFile(source)) {
+                    return { id: source };
                 }
 
-                const absolutePathInjectFile = path.resolve(
-                    context.bundler.outDir,
-                    INJECTED_FILE_PATH,
-                );
-
-                // Remove our assets.
-                log.debug(`Removing temporary file "${INJECTED_FILE_PATH}".`);
-                await rm(absolutePathInjectFile);
-            },
-        },
-        // Inject the file that will be home of all injected content.
-        // Each bundler has its own way to inject a file.
-        {
-            name: PLUGIN_NAME,
-            esbuild: {
-                setup(build) {
-                    const { initialOptions } = build;
-                    const absolutePathInjectFile = path.resolve(
-                        context.bundler.outDir,
-                        INJECTED_FILE_PATH,
-                    );
-
-                    // Inject the file in the build.
-                    // This is made safe for sub-builds by actually creating the file.
-                    initialOptions.inject = initialOptions.inject || [];
-                    initialOptions.inject.push(absolutePathInjectFile);
-                },
+                return null;
             },
-            webpack: (compiler) => {
-                const BannerPlugin =
-                    compiler?.webpack?.BannerPlugin ||
-                    bundler?.BannerPlugin ||
-                    bundler?.default?.BannerPlugin;
-
-                const ChunkGraph =
-                    compiler?.webpack?.ChunkGraph ||
-                    bundler?.ChunkGraph ||
-                    bundler?.default?.ChunkGraph;
-
-                if (!BannerPlugin) {
-                    log.error('Missing BannerPlugin');
+            loadInclude(id) {
+                if (isInjectionFile(id)) {
+                    return true;
                 }
 
-                // Intercept the compilation's ChunkGraph
-                let chunkGraph: InstanceType<typeof ChunkGraph>;
-                compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (compilation) => {
-                    compilation.hooks.afterChunks.tap(PLUGIN_NAME, () => {
-                        chunkGraph = compilation.chunkGraph;
-                    });
-                });
-
-                compiler.options.plugins = compiler.options.plugins || [];
-                compiler.options.plugins.push(
-                    new BannerPlugin({
-                        // Not wrapped in comments.
-                        raw: true,
-                        // Doesn't seem to work, but it's supposed to only add
-                        // the banner to entry modules.
-                        entryOnly: true,
-                        banner(data) {
-                            // In webpack5 we HAVE to use the chunkGraph.
-                            if (context.bundler.variant === '5') {
-                                if (
-                                    !chunkGraph ||
-                                    chunkGraph.getNumberOfEntryModules(data.chunk) === 0
-                                ) {
-                                    return '';
-                                }
-
-                                return getContentToInject();
-                            } else {
-                                if (!data.chunk?.hasEntryModule()) {
-                                    return '';
-                                }
-
-                                return getContentToInject();
-                            }
-                        },
-                    }),
-                );
+                return null;
             },
-            rspack: (compiler) => {
-                compiler.options.plugins = compiler.options.plugins || [];
-                compiler.options.plugins.push(
-                    new compiler.rspack.BannerPlugin({
-                        // Not wrapped in comments.
-                        raw: true,
-                        // Only entry modules.
-                        entryOnly: true,
-                        banner() {
-                            return getContentToInject();
-                        },
-                    }),
-                );
+            load(id) {
+                if (isInjectionFile(id)) {
+                    return {
+                        code: getContentToInject(contentsToInject[InjectPosition.MIDDLE]),
+                    };
+                }
+                return null;
             },
-            rollup: rollupInjectionPlugin,
-            vite: rollupInjectionPlugin,
         },
     ];
 
diff --git a/packages/plugins/injection/src/rollup.ts b/packages/plugins/injection/src/rollup.ts
new file mode 100644
index 000000000..ad1902f5d
--- /dev/null
+++ b/packages/plugins/injection/src/rollup.ts
@@ -0,0 +1,86 @@
+// 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 { isInjectionFile } from '@dd/core/helpers';
+import type { PluginOptions } from '@dd/core/types';
+import { InjectPosition } from '@dd/core/types';
+
+import { getContentToInject } from './helpers';
+import type { ContentsToInject } from './types';
+
+// Use "INJECTED_FILE" so it get flagged by isInjectionFile().
+const TO_INJECT_ID = INJECTED_FILE;
+const TO_INJECT_SUFFIX = '?inject-proxy';
+
+export const getRollupPlugin = (contentsToInject: ContentsToInject): PluginOptions['rollup'] => {
+    return {
+        banner(chunk) {
+            if (chunk.isEntry) {
+                // Can be empty.
+                return getContentToInject(contentsToInject[InjectPosition.BEFORE]);
+            }
+            return '';
+        },
+        async resolveId(source, importer, options) {
+            if (isInjectionFile(source)) {
+                // It is important that side effects are always respected for injections, otherwise using
+                // "treeshake.moduleSideEffects: false" may prevent the injection from being included.
+                return { id: source, moduleSideEffects: true };
+            }
+            if (options.isEntry && getContentToInject(contentsToInject[InjectPosition.MIDDLE])) {
+                // Determine what the actual entry would have been.
+                const resolution = await this.resolve(source, importer, options);
+                // If it cannot be resolved or is external, just return it so that Rollup can display an error
+                if (!resolution || resolution.external) {
+                    return resolution;
+                }
+                // In the load hook of the proxy, we need to know if the
+                // entry has a default export. There, however, we no longer
+                // have the full "resolution" object that may contain
+                // meta-data from other plugins that is only added on first
+                // load. Therefore we trigger loading here.
+                const moduleInfo = await this.load(resolution);
+                // We need to make sure side effects in the original entry
+                // point are respected even for
+                // treeshake.moduleSideEffects: false. "moduleSideEffects"
+                // is a writable property on ModuleInfo.
+                moduleInfo.moduleSideEffects = true;
+                // It is important that the new entry does not start with
+                // \0 and has the same directory as the original one to not
+                // mess up relative external import generation. Also
+                // keeping the name and just adding a "?query" to the end
+                // ensures that preserveModules will generate the original
+                // entry name for this entry.
+                return `${resolution.id}${TO_INJECT_SUFFIX}`;
+            }
+            return null;
+        },
+        load(id) {
+            if (isInjectionFile(id)) {
+                // Replace with injection content.
+                return getContentToInject(contentsToInject[InjectPosition.MIDDLE]);
+            }
+            if (id.endsWith(TO_INJECT_SUFFIX)) {
+                const entryId = id.slice(0, -TO_INJECT_SUFFIX.length);
+                // We know ModuleInfo.hasDefaultExport is reliable because we awaited this.load in resolveId
+                const info = this.getModuleInfo(entryId);
+                let code = `import ${JSON.stringify(TO_INJECT_ID)};\nexport * from ${JSON.stringify(entryId)};`;
+                // Namespace reexports do not reexport default, so we need special handling here
+                if (info?.hasDefaultExport) {
+                    code += `export { default } from ${JSON.stringify(entryId)};`;
+                }
+                return code;
+            }
+            return null;
+        },
+        footer(chunk) {
+            if (chunk.isEntry) {
+                // Can be empty.
+                return getContentToInject(contentsToInject[InjectPosition.AFTER]);
+            }
+            return '';
+        },
+    };
+};
diff --git a/packages/plugins/injection/src/types.ts b/packages/plugins/injection/src/types.ts
new file mode 100644
index 000000000..5b989b975
--- /dev/null
+++ b/packages/plugins/injection/src/types.ts
@@ -0,0 +1,14 @@
+// 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 { InjectPosition } from '@dd/core/types';
+
+export type ContentsToInject = Record<InjectPosition, Map<string, string>>;
+
+export type FileToInject = {
+    absolutePath: string;
+    filename: string;
+    toInject: Map<string, string>;
+};
+export type FilesToInject = Record<InjectPosition, FileToInject>;
diff --git a/packages/plugins/injection/src/xpack.ts b/packages/plugins/injection/src/xpack.ts
new file mode 100644
index 000000000..e4b1c6071
--- /dev/null
+++ b/packages/plugins/injection/src/xpack.ts
@@ -0,0 +1,170 @@
+// 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 { getUniqueId, outputFileSync, rm } from '@dd/core/helpers';
+import type { GlobalContext, Logger, PluginOptions, ToInjectItem } from '@dd/core/types';
+import { InjectPosition } from '@dd/core/types';
+import { createRequire } from 'module';
+import path from 'path';
+
+import { PLUGIN_NAME } from './constants';
+import { getContentToInject, addInjections } from './helpers';
+import type { ContentsToInject } from './types';
+
+// A way to get the correct ConcatSource from either the bundler (rspack and webpack 5)
+// or from 'webpack-sources' for webpack 4.
+const getConcatSource = (bundler: any): typeof import('webpack-sources').ConcatSource => {
+    if (!bundler?.sources?.ConcatSource) {
+        // We need to require it as if we were "webpack", hence the createRequire from 'webpack'.
+        // This way, we don't have to declare them in our (peer)dependencies and always use the one
+        // that is compatible with the 'webpack' we're currently using.
+        const webpackRequire = createRequire(require.resolve('webpack'));
+        return webpackRequire('webpack-sources').ConcatSource;
+    }
+    return bundler.sources.ConcatSource;
+};
+
+export const getXpackPlugin =
+    (
+        bundler: any,
+        log: Logger,
+        context: GlobalContext,
+        toInject: Map<string, ToInjectItem>,
+        contentsToInject: ContentsToInject,
+    ): PluginOptions['rspack'] & PluginOptions['webpack'] =>
+    (compiler) => {
+        const cache = new WeakMap();
+        const ConcatSource = getConcatSource(bundler);
+        const filePath = path.resolve(
+            context.bundler.outDir,
+            `${getUniqueId()}.${InjectPosition.MIDDLE}.${INJECTED_FILE}.js`,
+        );
+
+        // NOTE: RSpack MAY try to resolve the entry points before the loader is ready.
+        // There must be some race condition around this, because it's not always the case.
+        if (context.bundler.name === 'rspack') {
+            outputFileSync(filePath, '');
+        }
+
+        // Handle the InjectPosition.MIDDLE.
+        type Entry = typeof compiler.options.entry;
+        // TODO: Move this into @dd/core, add rspack/webpack types and tests.
+        const injectEntry = (initialEntry: Entry): Entry => {
+            const isWebpack4 = context.bundler.fullName === 'webpack4';
+
+            // Webpack 4 doesn't support the "import" property.
+            const injectedEntry = isWebpack4
+                ? filePath
+                : {
+                      import: [filePath],
+                  };
+
+            const objectInjection = (entry: Entry) => {
+                for (const [entryKey, entryValue] of Object.entries(entry)) {
+                    if (typeof entryValue === 'object') {
+                        entryValue.import = entryValue.import || [];
+                        entryValue.import.unshift(filePath);
+                    } else if (typeof entryValue === 'string') {
+                        // @ts-expect-error - Badly typed for strings.
+                        entry[entryKey] = [filePath, entryValue];
+                    } else if (Array.isArray(entryValue)) {
+                        entryValue.unshift(filePath);
+                    } else {
+                        log.error(`Invalid entry type: ${typeof entryValue}`);
+                    }
+                }
+            };
+
+            if (!initialEntry) {
+                return {
+                    // @ts-expect-error - Badly typed for strings.
+                    ddHelper: injectedEntry,
+                };
+            } else if (typeof initialEntry === 'function') {
+                // @ts-expect-error - This is webpack / rspack typing conflict.
+                return async () => {
+                    const originEntry = await initialEntry();
+                    objectInjection(originEntry);
+                    return originEntry;
+                };
+            } else if (typeof initialEntry === 'object') {
+                objectInjection(initialEntry);
+            } else if (typeof initialEntry === 'string') {
+                // @ts-expect-error - Badly typed for strings.
+                return [injectedEntry, initialEntry];
+            } else {
+                log.error(`Invalid entry type: ${typeof initialEntry}`);
+                return initialEntry;
+            }
+            return initialEntry;
+        };
+
+        // We need to prepare the injections before the build starts.
+        // Otherwise they'll be empty once resolved.
+        compiler.hooks.beforeRun.tapPromise(PLUGIN_NAME, async () => {
+            // Prepare the injections.
+            await addInjections(log, toInject, contentsToInject, context.cwd);
+        });
+
+        if (context.bundler.name === 'rspack') {
+            compiler.hooks.done.tapPromise(PLUGIN_NAME, async () => {
+                // Delete the fake file we created.
+                await rm(filePath);
+            });
+        }
+
+        // Handle the InjectPosition.START and InjectPosition.END.
+        // This is a re-implementation of the BannerPlugin,
+        // that is compatible with all versions of webpack and rspack,
+        // with both banner and footer.
+        compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
+            const hookCb = () => {
+                const banner = getContentToInject(contentsToInject[InjectPosition.BEFORE]);
+                const footer = getContentToInject(contentsToInject[InjectPosition.AFTER]);
+
+                for (const chunk of compilation.chunks) {
+                    if (!chunk.canBeInitial()) {
+                        continue;
+                    }
+
+                    for (const file of chunk.files) {
+                        compilation.updateAsset(file, (old) => {
+                            const cached = cache.get(old);
+
+                            // If anything changed, we need to re-create the source.
+                            if (!cached || cached.banner !== banner || cached.footer !== footer) {
+                                const source = new ConcatSource(
+                                    banner,
+                                    '\n',
+                                    // @ts-expect-error - This is webpack / rspack typing conflict.
+                                    old,
+                                    '\n',
+                                    footer,
+                                );
+
+                                // Cache the result.
+                                cache.set(old, { source, banner, footer });
+                                return source;
+                            }
+
+                            return cached.source;
+                        });
+                    }
+                }
+            };
+
+            if (compilation.hooks.processAssets) {
+                const stage = bundler.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS;
+                compilation.hooks.processAssets.tap({ name: PLUGIN_NAME, stage }, hookCb);
+            } else {
+                // @ts-expect-error - "optimizeChunkAssets" is for webpack 4.
+                compilation.hooks.optimizeChunkAssets.tap({ name: PLUGIN_NAME }, hookCb);
+            }
+        });
+
+        // We inject the new entry.
+        const newEntry = injectEntry(compiler.options.entry);
+        compiler.options.entry = newEntry;
+    };
diff --git a/packages/plugins/telemetry/src/common/output/text.ts b/packages/plugins/telemetry/src/common/output/text.ts
index 1be2299ec..fac1b8e78 100644
--- a/packages/plugins/telemetry/src/common/output/text.ts
+++ b/packages/plugins/telemetry/src/common/output/text.ts
@@ -2,9 +2,8 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { formatDuration, truncateString } from '@dd/core/helpers';
+import { formatDuration, serializeBuildReport, truncateString } from '@dd/core/helpers';
 import type { Logger, Entry, GlobalContext, Output } from '@dd/core/types';
-import { serializeBuildReport } from '@dd/internal-build-report-plugin/helpers';
 import chalk from 'chalk';
 import prettyBytes from 'pretty-bytes';
 
diff --git a/packages/published/esbuild-plugin/README.md b/packages/published/esbuild-plugin/README.md
index d90e66944..fca41be44 100644
--- a/packages/published/esbuild-plugin/README.md
+++ b/packages/published/esbuild-plugin/README.md
@@ -1,6 +1,6 @@
-# Datadog ESBuild Plugin
+# Datadog esbuild Plugin
 
-A ESBuild plugin to interact with Datadog from your ESBuild builds.
+A esbuild plugin to interact with Datadog from your ESBuild builds.
 
 ## Installation
 
diff --git a/packages/published/esbuild-plugin/package.json b/packages/published/esbuild-plugin/package.json
index b20d456d7..fd291184d 100644
--- a/packages/published/esbuild-plugin/package.json
+++ b/packages/published/esbuild-plugin/package.json
@@ -1,7 +1,7 @@
 {
     "name": "@datadog/esbuild-plugin",
     "packageManager": "yarn@4.0.2",
-    "version": "2.4.2",
+    "version": "2.5.0",
     "license": "MIT",
     "author": "Datadog",
     "description": "Datadog ESBuild Plugin",
@@ -20,7 +20,9 @@
     },
     "main": "./dist/src/index.js",
     "module": "./dist/src/index.mjs",
+    "types": "./dist/src/index.d.ts",
     "exports": {
+        "./dist/src": "./dist/src/index.js",
         ".": "./src/index.ts"
     },
     "publishConfig": {
@@ -63,8 +65,10 @@
         "@dd/tools": "workspace:*",
         "@rollup/plugin-babel": "6.0.4",
         "@rollup/plugin-commonjs": "28.0.1",
+        "@rollup/plugin-esm-shim": "0.1.7",
         "@rollup/plugin-json": "6.1.0",
         "@rollup/plugin-node-resolve": "15.3.0",
+        "@rollup/plugin-terser": "0.4.4",
         "@types/babel__core": "^7",
         "@types/babel__preset-env": "^7",
         "esbuild": "0.24.0",
diff --git a/packages/published/esbuild-plugin/src/index.ts b/packages/published/esbuild-plugin/src/index.ts
index a02a95466..c0687c8b7 100644
--- a/packages/published/esbuild-plugin/src/index.ts
+++ b/packages/published/esbuild-plugin/src/index.ts
@@ -7,24 +7,29 @@
 // will be updated using the 'yarn cli integrity' command.
 
 import type { Options } from '@dd/core/types';
+import type {
+    // #types-export-injection-marker
+    ErrorTrackingTypes,
+    TelemetryTypes,
+    // #types-export-injection-marker
+} from '@dd/factory';
 import * as factory from '@dd/factory';
 import esbuild from 'esbuild';
 
 import pkg from '../package.json';
 
-export const datadogEsbuildPlugin = factory.buildPluginFactory({
-    bundler: esbuild,
-    version: pkg.version,
-}).esbuild;
-
 export type EsbuildPluginOptions = Options;
-
 export type {
     // #types-export-injection-marker
-    RumTypes,
+    ErrorTrackingTypes,
     TelemetryTypes,
     // #types-export-injection-marker
-} from '@dd/factory';
+};
+
+export const datadogEsbuildPlugin = factory.buildPluginFactory({
+    bundler: esbuild,
+    version: pkg.version,
+}).esbuild;
 
 export const version = pkg.version;
 export const helpers = factory.helpers;
diff --git a/packages/published/rollup-plugin/package.json b/packages/published/rollup-plugin/package.json
index 401c917f1..9a88e5594 100644
--- a/packages/published/rollup-plugin/package.json
+++ b/packages/published/rollup-plugin/package.json
@@ -1,7 +1,7 @@
 {
     "name": "@datadog/rollup-plugin",
     "packageManager": "yarn@4.0.2",
-    "version": "2.4.2",
+    "version": "2.5.0",
     "license": "MIT",
     "author": "Datadog",
     "description": "Datadog Rollup Plugin",
@@ -20,7 +20,9 @@
     },
     "main": "./dist/src/index.js",
     "module": "./dist/src/index.mjs",
+    "types": "./dist/src/index.d.ts",
     "exports": {
+        "./dist/src": "./dist/src/index.js",
         ".": "./src/index.ts"
     },
     "publishConfig": {
@@ -63,8 +65,10 @@
         "@dd/tools": "workspace:*",
         "@rollup/plugin-babel": "6.0.4",
         "@rollup/plugin-commonjs": "28.0.1",
+        "@rollup/plugin-esm-shim": "0.1.7",
         "@rollup/plugin-json": "6.1.0",
         "@rollup/plugin-node-resolve": "15.3.0",
+        "@rollup/plugin-terser": "0.4.4",
         "@types/babel__core": "^7",
         "@types/babel__preset-env": "^7",
         "esbuild": "0.24.0",
diff --git a/packages/published/rollup-plugin/src/index.ts b/packages/published/rollup-plugin/src/index.ts
index c0e9eaf86..a1d657111 100644
--- a/packages/published/rollup-plugin/src/index.ts
+++ b/packages/published/rollup-plugin/src/index.ts
@@ -7,24 +7,29 @@
 // will be updated using the 'yarn cli integrity' command.
 
 import type { Options } from '@dd/core/types';
+import type {
+    // #types-export-injection-marker
+    ErrorTrackingTypes,
+    TelemetryTypes,
+    // #types-export-injection-marker
+} from '@dd/factory';
 import * as factory from '@dd/factory';
 import rollup from 'rollup';
 
 import pkg from '../package.json';
 
-export const datadogRollupPlugin = factory.buildPluginFactory({
-    bundler: rollup,
-    version: pkg.version,
-}).rollup;
-
 export type RollupPluginOptions = Options;
-
 export type {
     // #types-export-injection-marker
-    RumTypes,
+    ErrorTrackingTypes,
     TelemetryTypes,
     // #types-export-injection-marker
-} from '@dd/factory';
+};
+
+export const datadogRollupPlugin = factory.buildPluginFactory({
+    bundler: rollup,
+    version: pkg.version,
+}).rollup;
 
 export const version = pkg.version;
 export const helpers = factory.helpers;
diff --git a/packages/published/rspack-plugin/package.json b/packages/published/rspack-plugin/package.json
index 10f5c57a1..ad3fcf395 100644
--- a/packages/published/rspack-plugin/package.json
+++ b/packages/published/rspack-plugin/package.json
@@ -1,7 +1,7 @@
 {
     "name": "@datadog/rspack-plugin",
     "packageManager": "yarn@4.0.2",
-    "version": "2.4.2",
+    "version": "2.5.0",
     "license": "MIT",
     "author": "Datadog",
     "description": "Datadog Rspack Plugin",
@@ -20,7 +20,9 @@
     },
     "main": "./dist/src/index.js",
     "module": "./dist/src/index.mjs",
+    "types": "./dist/src/index.d.ts",
     "exports": {
+        "./dist/src": "./dist/src/index.js",
         ".": "./src/index.ts"
     },
     "publishConfig": {
@@ -63,8 +65,10 @@
         "@dd/tools": "workspace:*",
         "@rollup/plugin-babel": "6.0.4",
         "@rollup/plugin-commonjs": "28.0.1",
+        "@rollup/plugin-esm-shim": "0.1.7",
         "@rollup/plugin-json": "6.1.0",
         "@rollup/plugin-node-resolve": "15.3.0",
+        "@rollup/plugin-terser": "0.4.4",
         "@types/babel__core": "^7",
         "@types/babel__preset-env": "^7",
         "esbuild": "0.24.0",
diff --git a/packages/published/rspack-plugin/src/index.ts b/packages/published/rspack-plugin/src/index.ts
index e5496f436..03ec3d2ac 100644
--- a/packages/published/rspack-plugin/src/index.ts
+++ b/packages/published/rspack-plugin/src/index.ts
@@ -7,24 +7,29 @@
 // will be updated using the 'yarn cli integrity' command.
 
 import type { Options } from '@dd/core/types';
+import type {
+    // #types-export-injection-marker
+    ErrorTrackingTypes,
+    TelemetryTypes,
+    // #types-export-injection-marker
+} from '@dd/factory';
 import * as factory from '@dd/factory';
 import rspack from '@rspack/core';
 
 import pkg from '../package.json';
 
-export const datadogRspackPlugin = factory.buildPluginFactory({
-    bundler: rspack,
-    version: pkg.version,
-}).rspack;
-
 export type RspackPluginOptions = Options;
-
 export type {
     // #types-export-injection-marker
-    RumTypes,
+    ErrorTrackingTypes,
     TelemetryTypes,
     // #types-export-injection-marker
-} from '@dd/factory';
+};
+
+export const datadogRspackPlugin = factory.buildPluginFactory({
+    bundler: rspack,
+    version: pkg.version,
+}).rspack;
 
 export const version = pkg.version;
 export const helpers = factory.helpers;
diff --git a/packages/published/vite-plugin/package.json b/packages/published/vite-plugin/package.json
index 3ddd6e079..621fbca5a 100644
--- a/packages/published/vite-plugin/package.json
+++ b/packages/published/vite-plugin/package.json
@@ -1,7 +1,7 @@
 {
     "name": "@datadog/vite-plugin",
     "packageManager": "yarn@4.0.2",
-    "version": "2.4.2",
+    "version": "2.5.0",
     "license": "MIT",
     "author": "Datadog",
     "description": "Datadog Vite Plugin",
@@ -20,7 +20,9 @@
     },
     "main": "./dist/src/index.js",
     "module": "./dist/src/index.mjs",
+    "types": "./dist/src/index.d.ts",
     "exports": {
+        "./dist/src": "./dist/src/index.js",
         ".": "./src/index.ts"
     },
     "publishConfig": {
@@ -63,8 +65,10 @@
         "@dd/tools": "workspace:*",
         "@rollup/plugin-babel": "6.0.4",
         "@rollup/plugin-commonjs": "28.0.1",
+        "@rollup/plugin-esm-shim": "0.1.7",
         "@rollup/plugin-json": "6.1.0",
         "@rollup/plugin-node-resolve": "15.3.0",
+        "@rollup/plugin-terser": "0.4.4",
         "@types/babel__core": "^7",
         "@types/babel__preset-env": "^7",
         "esbuild": "0.24.0",
diff --git a/packages/published/vite-plugin/src/index.ts b/packages/published/vite-plugin/src/index.ts
index 684af94d7..64e64e863 100644
--- a/packages/published/vite-plugin/src/index.ts
+++ b/packages/published/vite-plugin/src/index.ts
@@ -7,24 +7,29 @@
 // will be updated using the 'yarn cli integrity' command.
 
 import type { Options } from '@dd/core/types';
+import type {
+    // #types-export-injection-marker
+    ErrorTrackingTypes,
+    TelemetryTypes,
+    // #types-export-injection-marker
+} from '@dd/factory';
 import * as factory from '@dd/factory';
 import * as vite from 'vite';
 
 import pkg from '../package.json';
 
-export const datadogVitePlugin = factory.buildPluginFactory({
-    bundler: vite,
-    version: pkg.version,
-}).vite;
-
 export type VitePluginOptions = Options;
-
 export type {
     // #types-export-injection-marker
-    RumTypes,
+    ErrorTrackingTypes,
     TelemetryTypes,
     // #types-export-injection-marker
-} from '@dd/factory';
+};
+
+export const datadogVitePlugin = factory.buildPluginFactory({
+    bundler: vite,
+    version: pkg.version,
+}).vite;
 
 export const version = pkg.version;
 export const helpers = factory.helpers;
diff --git a/packages/published/webpack-plugin/package.json b/packages/published/webpack-plugin/package.json
index 4f3cb664a..f4a189514 100644
--- a/packages/published/webpack-plugin/package.json
+++ b/packages/published/webpack-plugin/package.json
@@ -1,7 +1,7 @@
 {
     "name": "@datadog/webpack-plugin",
     "packageManager": "yarn@4.0.2",
-    "version": "2.4.2",
+    "version": "2.5.0",
     "license": "MIT",
     "author": "Datadog",
     "description": "Datadog Webpack Plugin",
@@ -20,7 +20,9 @@
     },
     "main": "./dist/src/index.js",
     "module": "./dist/src/index.mjs",
+    "types": "./dist/src/index.d.ts",
     "exports": {
+        "./dist/src/*": "./dist/src/*",
         ".": "./src/index.ts"
     },
     "publishConfig": {
@@ -63,8 +65,10 @@
         "@dd/tools": "workspace:*",
         "@rollup/plugin-babel": "6.0.4",
         "@rollup/plugin-commonjs": "28.0.1",
+        "@rollup/plugin-esm-shim": "0.1.7",
         "@rollup/plugin-json": "6.1.0",
         "@rollup/plugin-node-resolve": "15.3.0",
+        "@rollup/plugin-terser": "0.4.4",
         "@types/babel__core": "^7",
         "@types/babel__preset-env": "^7",
         "esbuild": "0.24.0",
diff --git a/packages/published/webpack-plugin/src/index.ts b/packages/published/webpack-plugin/src/index.ts
index 501d34afe..2e338f4bf 100644
--- a/packages/published/webpack-plugin/src/index.ts
+++ b/packages/published/webpack-plugin/src/index.ts
@@ -7,24 +7,29 @@
 // will be updated using the 'yarn cli integrity' command.
 
 import type { Options } from '@dd/core/types';
+import type {
+    // #types-export-injection-marker
+    ErrorTrackingTypes,
+    TelemetryTypes,
+    // #types-export-injection-marker
+} from '@dd/factory';
 import * as factory from '@dd/factory';
 import webpack from 'webpack';
 
 import pkg from '../package.json';
 
-export const datadogWebpackPlugin = factory.buildPluginFactory({
-    bundler: webpack,
-    version: pkg.version,
-}).webpack;
-
 export type WebpackPluginOptions = Options;
-
 export type {
     // #types-export-injection-marker
-    RumTypes,
+    ErrorTrackingTypes,
     TelemetryTypes,
     // #types-export-injection-marker
-} from '@dd/factory';
+};
+
+export const datadogWebpackPlugin = factory.buildPluginFactory({
+    bundler: webpack,
+    version: pkg.version,
+}).webpack;
 
 export const version = pkg.version;
 export const helpers = factory.helpers;
diff --git a/packages/tests/.gitignore b/packages/tests/.gitignore
new file mode 100644
index 000000000..584439da2
--- /dev/null
+++ b/packages/tests/.gitignore
@@ -0,0 +1,5 @@
+# Playwright stuffs
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/packages/tests/README.md b/packages/tests/README.md
index 46c15d778..59d81e2c2 100644
--- a/packages/tests/README.md
+++ b/packages/tests/README.md
@@ -10,41 +10,39 @@ Especially useful for having mock projects, built with specific bundlers and run
 <!-- This is auto generated with yarn cli integrity -->
 
 <!-- #toc -->
--   [Run all the tests](#run-all-the-tests)
--   [Run all the tests with all the logs](#run-all-the-tests-with-all-the-logs)
--   [Debug a test](#debug-a-test)
--   [Test a plugin](#test-a-plugin)
-    -   [Bootstrapping your test](#bootstrapping-your-test)
-    -   [Bundlers](#bundlers)
-    -   [More complex projects](#more-complex-projects)
-    -   [Work with the global context](#work-with-the-global-context)
+-   [Unit tests](#unit-tests)
+    -   [Run](#run)
+    -   [Debug](#debug)
+    -   [Test a plugin](#test-a-plugin)
+-   [End to End tests](#end-to-end-tests)
+    -   [Run](#run)
+    -   [Debug](#debug)
 <!-- #toc -->
 
-## Run all the tests
+## Unit tests
 
-```bash
-yarn test
-```
-
-## Run all the tests with all the logs
+Place your tests in `packages/tests/src/unit/plugins/<your plugin>/**/*.test.ts`.<br/>
 
-By default, jest is in silent mode and won't show any logs.
+### Run
 
 ```bash
-yarn test:noisy
+yarn test:unit
+yarn workspace @dd/tests test
 ```
 
-## Debug a test
+You can use [jest flags](https://jestjs.io/docs/cli) directly after the command.
+
+### Debug
 
 You can target a single file the same as if you were using Jest's CLI.
 
 Within your test you can then use `.only` or `.skip` to target a single test in particular.
 
 ```bash
-yarn test:noisy packages/tests/...
+yarn test:unit packages/tests/...
 ```
 
-## Test a plugin
+### Test a plugin
 
 Once you have your plugin ready, you can test it in two ways, both are not exclusive.
 
@@ -54,7 +52,7 @@ This doesn't need much explanation as it is pretty straight-forward.
 Or the **integration** way, which will test the plugin within the whole ecosystem, but is a bit more involved to setup correctly.<br/>
 Let's talk about this a bit more.
 
-### Bootstrapping your test
+#### Bootstrapping your test
 
 Here's a bootstrap to get you going:
 
@@ -86,34 +84,35 @@ describe('My very awesome plugin', () => {
 });
 ```
 
-### Bundlers
+#### Bundlers
 
-We currently support `webpack4`, `webpack5`, `esbuild`, `rollup` and `vite`.<br/>
+We currently support `webpack4`, `webpack5`, `rspack`, `esbuild`, `rollup` and `vite`.<br/>
 So we need to ensure that our plugin works everywhere.
 
-When you use `runBundlers()` in your setup (usually `beforeAll()`), it will run the build of [a very basic default mock project](/packages/tests/src/_jest/fixtures/main.js).<br/>
+When you use `runBundlers()` in your setup (usually `beforeAll()`), it will run the build of [a very basic default mock project](/packages/tests/src/_jest/fixtures/easy_project/main.js).<br/>
 Since it's building in a seeded directory, to avoid any collision, it will also return a cleanup function, that you'll need to use in your teardown (usually `afterAll()`).
 
 During development, you may want to target a specific bundler, to reduce noise from the others.<br/>
 For this, you can use the `--bundlers=<name>,<name>` flag when running your tests:
 
 ```bash
-yarn test:noisy packages/tests/... --bundlers=webpack4,esbuild
+yarn test:unit packages/tests/... --bundlers=webpack4,esbuild
 ```
 
 If you want to keep the built files for debugging purpose, you can use the `--cleanup=0` parameter:
 
 ```bash
-yarn test:noisy packages/tests/... --cleanup=0
+yarn test:unit packages/tests/... --cleanup=0
 ```
 
 If you want to also build the bundlers you're targeting, you can use the `--build=1` parameter:
 
 ```bash
-yarn test:noisy packages/tests/... --build=1
+# Will also build both webpack and esbuild plugins before running the tests.
+yarn test:unit packages/tests/... --build=1 --bundlers=webpack4,esbuild
 ```
 
-### More complex projects
+#### More complex projects
 
 We also have [a more complex project](/packages/tests/src/_jest/fixtures/project), with third parties dependencies for instance, that you can use with the `getComplexBuildOverrides()` function.<br/>
 To be used as follow:
@@ -133,7 +132,6 @@ It will return the array of entries it created.
 Here's how you'd go with it:
 
 ```typescript
-import { getWebpack4Entries } from '@dd/tests/_jest/helpers/xpackConfigs';
 import { generateProject } from '@dd/tests/_jest/helpers/generateMassiveProject';
 import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
 import { runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
@@ -154,11 +152,7 @@ describe('Some very massive project', () => {
             },
             // Mode production makes the build waaaaayyyyy too slow.
             webpack5: { mode: 'none', entry: entries },
-            webpack4: {
-                mode: 'none',
-                // Webpack4 needs some help for pnp resolutions.
-                entry: getWebpack4Entries(entries),
-            },
+            webpack4: { mode: 'none', entry: entries },
         };
 
         cleanup = await runBundlers(defaultPluginOptions, bundlerOverrides);
@@ -174,12 +168,12 @@ describe('Some very massive project', () => {
 > `generateProject()` is not persistent.
 > So for now it's only to be used to debug your plugin when necessary.
 
-### Work with the global context
+#### Work with the global context
 
 The global context is pretty nifty to share data between plugins.<br/>
 But, it is a mutable object, so you'll have to keep that in mind when testing around it.
 
-The best way would be to freeze the content you need to test, at the moment you want to test it:
+The best way would be to freeze the content you need to test, at the moment you want to test it, for instance, to capture the initial context using `JSON.parse(JSON.stringify(context.bundler))` to freeze it:
 
 ```typescript
 import type { GlobalContext, Options } from '@dd/core/types';
@@ -198,7 +192,7 @@ describe('Global Context Plugin', () => {
             customPlugins: (opts, context) => {
                 const bundlerName = context.bundler.fullName;
                 // Freeze the context here, to verify what's available during initialization.
-                initialContexts[bundlerName] = JSON.parse(JSON.stringify(context));
+                initialContexts[bundlerName] = JSON.parse(JSON.stringify(context.bundler));
                 return [];
             },
         };
@@ -233,10 +227,7 @@ buildReports[bundlerName] = unserializeBuildReport(serializeBuildReport(context.
 Giving the following, more involved example:
 
 ```typescript
-import {
-    serializeBuildReport,
-    unserializeBuildReport,
-} from '@dd/core/plugins/build-report/helpers';
+import { serializeBuildReport, unserializeBuildReport } from '@dd/core/helpers';
 import type { BuildReport, Options } from '@dd/core/types';
 import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
 import type { CleanupFn } from '@dd/tests/_jest/helpers/runBundlers';
@@ -276,3 +267,66 @@ describe('Build Reports', () => {
     });
 });
 ```
+
+## End to End tests
+
+We use [Playwright](https://playwright.dev/) for our end to end tests.
+
+Place your tests in `packages/tests/src/e2e/**/*.spec.ts`.
+
+The test run takes care of building the `@datadog/*-plugin` packages locally.<br/>
+You can bypass this build step prefixing your command with `CI=1 yarn [...]` reducing the duration of the run.
+
+### Run
+
+```bash
+yarn test:e2e
+```
+
+You can use [Playwright flags](https://playwright.dev/docs/running-tests#command-line) directly after the command.
+
+### Debug
+
+#### From the CI
+
+If your CI job fails, you can download the `playwright` artifact of the run, at the bottom of the summary page.
+
+Once downloaded, extract it by double clicking on it and run the following command:
+
+```bash
+yarn workspace @dd/tests playwright show-report ~/Downloads/playwright/playwright-report
+```
+
+#### Locally
+
+Run the test with the UI enabled:
+
+```bash
+yarn test:e2e --ui
+```
+
+Then, you can use the Playwright UI to debug your test.
+
+More information on the [Playwright documentation](https://playwright.dev/docs/running-tests#command-line).
+
+
+#### Run a specific bundler or browser
+
+There is one project for each bundler / browser combination.<br/>
+The naming follows the pattern `<browser> | <bundler>` eg. `chrome | webpack4`.
+
+You can use the `--project` flag to target a specific project (or multiple projects):
+
+```bash
+yarn test:e2e --project "chrome | webpack4" --project "firefox | esbuild"
+```
+
+It also supports glob patterns:
+
+```bash
+# Run all the bundlers for the chrome browser.
+yarn test:e2e --project "chrome | *"
+
+# Run all browsers for the webpack4 bundler.
+yarn test:e2e --project "* | webpack4"
+```
diff --git a/packages/tests/jest.config.js b/packages/tests/jest.config.js
index 57fd27844..796986f99 100644
--- a/packages/tests/jest.config.js
+++ b/packages/tests/jest.config.js
@@ -7,10 +7,9 @@ module.exports = {
     clearMocks: true,
     globalSetup: '<rootDir>/src/_jest/globalSetup.ts',
     preset: 'ts-jest/presets/js-with-ts',
-    reporters: [['default', { summaryThreshold: 2 }]],
     // Without it, vite import is silently crashing the process with code SIGHUP 129
     resetModules: true,
-    roots: ['./src/'],
+    roots: ['./src/unit/'],
     setupFilesAfterEnv: ['<rootDir>/src/_jest/setupAfterEnv.ts'],
     testEnvironment: 'node',
     testMatch: ['**/*.test.*'],
diff --git a/packages/tests/package.json b/packages/tests/package.json
index 422c50efd..c757b4208 100644
--- a/packages/tests/package.json
+++ b/packages/tests/package.json
@@ -14,28 +14,29 @@
     "exports": {
         "./*": "./src/*.ts",
         "./_jest/fixtures/*": "./src/_jest/fixtures/*",
-        "./_jest/helpers/*": "./src/_jest/helpers/*.ts",
-        "./plugins/telemetry/*": "./src/plugins/telemetry/*.ts"
+        "./_jest/helpers/*": "./src/_jest/helpers/*.ts"
     },
     "scripts": {
         "build": "yarn clean && tsc",
         "clean": "rm -rf dist",
-        "test": "yarn test:noisy --silent",
-        "test:noisy": "JEST_CONFIG_TRANSPILE_ONLY=true VITE_CJS_IGNORE_WARNING=true NODE_OPTIONS=\"--openssl-legacy-provider --experimental-vm-modules ${NODE_OPTIONS:-}\" jest --verbose",
+        "test:e2e": "FORCE_COLOR=true playwright test",
+        "test:unit": "FORCE_COLOR=true JEST_CONFIG_TRANSPILE_ONLY=true VITE_CJS_IGNORE_WARNING=true NODE_OPTIONS=\"--openssl-legacy-provider --experimental-vm-modules ${NODE_OPTIONS:-}\" jest",
         "typecheck": "tsc --noEmit"
     },
     "dependencies": {
         "@datadog/esbuild-plugin": "workspace:*",
         "@datadog/rollup-plugin": "workspace:*",
+        "@datadog/rspack-plugin": "workspace:*",
         "@datadog/vite-plugin": "workspace:*",
         "@datadog/webpack-plugin": "workspace:*",
         "@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:*",
         "@dd/internal-injection-plugin": "workspace:*",
-        "@dd/rum-plugin": "workspace:*",
         "@dd/telemetry-plugin": "workspace:*",
+        "@dd/tools": "workspace:*",
         "@rollup/plugin-commonjs": "28.0.1",
         "clipanion": "4.0.0-rc.3",
         "glob": "11.0.0",
@@ -43,6 +44,7 @@
         "ts-jest": "29.1.2"
     },
     "devDependencies": {
+        "@playwright/test": "1.49.1",
         "@rspack/core": "1.1.2",
         "@types/faker": "5.5.9",
         "@types/jest": "29.5.12",
diff --git a/packages/tests/playwright.config.ts b/packages/tests/playwright.config.ts
new file mode 100644
index 000000000..4251ae213
--- /dev/null
+++ b/packages/tests/playwright.config.ts
@@ -0,0 +1,72 @@
+// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants';
+import { DEV_SERVER_PORT, DEV_SERVER_URL, PUBLIC_DIR } from '@dd/tests/_playwright/constants';
+import { getRequestedBundlers } from '@dd/tests/_playwright/helpers/requestedBundlers';
+import type { TestOptions } from '@dd/tests/_playwright/testParams';
+import { ROOT } from '@dd/tools/constants';
+import { defineConfig, devices } from '@playwright/test';
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig<TestOptions>({
+    testDir: './src/e2e',
+    /* Run tests in files in parallel */
+    fullyParallel: true,
+    /* Fail the build on CI if you accidentally left test.only in the source code. */
+    forbidOnly: !!process.env.CI,
+    /* Retry on CI only */
+    retries: process.env.CI ? 2 : 0,
+    /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+    reporter: process.env.CI ? 'html' : 'list',
+    /* Path to file run before all the tests. See https://playwright.dev/docs/test-global-setup-teardown */
+    globalSetup: require.resolve('./src/_playwright/globalSetup.ts'),
+    use: {
+        bundlers: getRequestedBundlers(),
+        trace: 'retain-on-failure',
+    },
+    timeout: 5_000,
+    /* Configure projects for each bundler */
+    // TODO Also build and test for ESM.
+    projects: FULL_NAME_BUNDLERS.map((bundler) => [
+        {
+            name: `chrome | ${bundler}`,
+            use: {
+                ...devices['Desktop Chrome'],
+                bundler,
+            },
+        },
+        {
+            name: `firefox | ${bundler}`,
+            use: {
+                ...devices['Desktop Firefox'],
+                bundler,
+            },
+        },
+        {
+            name: `edge | ${bundler}`,
+            use: {
+                ...devices['Desktop Edge'],
+                bundler,
+            },
+        },
+        {
+            name: `safari | ${bundler}`,
+            use: {
+                ...devices['Desktop Safari'],
+                bundler,
+            },
+        },
+    ]).flat(),
+
+    /* Run your local dev server before starting the tests */
+    webServer: {
+        command: `yarn cli dev-server --root=${PUBLIC_DIR} --port=${DEV_SERVER_PORT}`,
+        cwd: ROOT,
+        url: DEV_SERVER_URL,
+        reuseExistingServer: !process.env.CI,
+    },
+});
diff --git a/packages/tests/src/_jest/fixtures/.gitignore b/packages/tests/src/_jest/fixtures/.gitignore
index 38ce858cb..5e2c6aa88 100644
--- a/packages/tests/src/_jest/fixtures/.gitignore
+++ b/packages/tests/src/_jest/fixtures/.gitignore
@@ -1 +1,11 @@
 massiveProject
+yarn-error.log
+.yarn/*
+!.yarn/cache
+!.yarn/patches
+!.yarn/releases
+!.yarn/plugins
+!.vscode
+
+node_modules/
+dist/
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/@remix-run-router-npm-1.21.0-22ebfe59d7-cf0fb69d19.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/@remix-run-router-npm-1.21.0-22ebfe59d7-cf0fb69d19.zip
new file mode 100644
index 000000000..d57ee4e82
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/@remix-run-router-npm-1.21.0-22ebfe59d7-cf0fb69d19.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip
new file mode 100644
index 000000000..4ffdcc494
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/chalk-npm-2.3.1-f10c7b2b06-53f7346b01.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/chalk-npm-2.3.1-f10c7b2b06-53f7346b01.zip
new file mode 100644
index 000000000..34fc41f20
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/chalk-npm-2.3.1-f10c7b2b06-53f7346b01.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/color-convert-npm-1.9.3-1fe690075e-ffa3190250.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/color-convert-npm-1.9.3-1fe690075e-ffa3190250.zip
new file mode 100644
index 000000000..c4d6feded
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/color-convert-npm-1.9.3-1fe690075e-ffa3190250.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/color-name-npm-1.1.3-728b7b5d39-09c5d3e33d.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/color-name-npm-1.1.3-728b7b5d39-09c5d3e33d.zip
new file mode 100644
index 000000000..f158de9e2
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/color-name-npm-1.1.3-728b7b5d39-09c5d3e33d.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/escape-string-regexp-npm-1.0.5-3284de402f-6092fda75c.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/escape-string-regexp-npm-1.0.5-3284de402f-6092fda75c.zip
new file mode 100644
index 000000000..b7ea3be14
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/escape-string-regexp-npm-1.0.5-3284de402f-6092fda75c.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/has-flag-npm-3.0.0-16ac11fe05-4a15638b45.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/has-flag-npm-3.0.0-16ac11fe05-4a15638b45.zip
new file mode 100644
index 000000000..60eafa65f
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/has-flag-npm-3.0.0-16ac11fe05-4a15638b45.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-dom-npm-19.0.0-b7981c573e-aa64a2f199.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-dom-npm-19.0.0-b7981c573e-aa64a2f199.zip
new file mode 100644
index 000000000..6ccb14bd6
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-dom-npm-19.0.0-b7981c573e-aa64a2f199.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-npm-19.0.0-e33c9aa1c0-2490969c50.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-npm-19.0.0-e33c9aa1c0-2490969c50.zip
new file mode 100644
index 000000000..93d264328
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-npm-19.0.0-e33c9aa1c0-2490969c50.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-dom-npm-6.28.0-3bd3cd7fc0-e637825132.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-dom-npm-6.28.0-3bd3cd7fc0-e637825132.zip
new file mode 100644
index 000000000..6fde01be5
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-dom-npm-6.28.0-3bd3cd7fc0-e637825132.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-npm-6.28.0-8611821701-f021a64451.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-npm-6.28.0-8611821701-f021a64451.zip
new file mode 100644
index 000000000..bc89de389
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/react-router-npm-6.28.0-8611821701-f021a64451.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/scheduler-npm-0.25.0-f89e6cad04-e661e38503.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/scheduler-npm-0.25.0-f89e6cad04-e661e38503.zip
new file mode 100644
index 000000000..05d831f8c
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/scheduler-npm-0.25.0-f89e6cad04-e661e38503.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarn/cache/supports-color-npm-5.5.0-183ac537bc-5f505c6fa3.zip b/packages/tests/src/_jest/fixtures/.yarn/cache/supports-color-npm-5.5.0-183ac537bc-5f505c6fa3.zip
new file mode 100644
index 000000000..55a34c67d
Binary files /dev/null and b/packages/tests/src/_jest/fixtures/.yarn/cache/supports-color-npm-5.5.0-183ac537bc-5f505c6fa3.zip differ
diff --git a/packages/tests/src/_jest/fixtures/.yarnrc.yml b/packages/tests/src/_jest/fixtures/.yarnrc.yml
new file mode 100644
index 000000000..2aa2d15c7
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/.yarnrc.yml
@@ -0,0 +1,4 @@
+compressionLevel: mixed
+defaultSemverRangePrefix: ""
+enableGlobalCache: false
+nodeLinker: node-modules
diff --git a/packages/tests/src/_jest/fixtures/main.js b/packages/tests/src/_jest/fixtures/easy_project/main.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/main.js
rename to packages/tests/src/_jest/fixtures/easy_project/main.js
diff --git a/packages/tests/src/_jest/fixtures/easy_project/package.json b/packages/tests/src/_jest/fixtures/easy_project/package.json
new file mode 100644
index 000000000..fe57ee635
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/easy_project/package.json
@@ -0,0 +1,13 @@
+{
+    "name": "@tests/easy_project",
+    "private": true,
+    "license": "MIT",
+    "author": "Datadog",
+    "packageManager": "yarn@4.2.1",
+    "devDependencies": {
+        "chalk": "2.3.1",
+        "react": "19.0.0",
+        "react-dom": "19.0.0",
+        "react-router-dom": "6.28.0"
+    }
+}
diff --git a/packages/tests/src/_jest/fixtures/project/empty.js b/packages/tests/src/_jest/fixtures/empty.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/project/empty.js
rename to packages/tests/src/_jest/fixtures/empty.js
diff --git a/packages/tests/src/_jest/fixtures/fake-file-to-inject-after.js b/packages/tests/src/_jest/fixtures/fake-file-to-inject-after.js
new file mode 100644
index 000000000..2645ec5a0
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/fake-file-to-inject-after.js
@@ -0,0 +1,5 @@
+// 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.
+
+console.log("Hello injection from local file in after.");
\ No newline at end of file
diff --git a/packages/tests/src/_jest/fixtures/fake-file-to-inject-before.js b/packages/tests/src/_jest/fixtures/fake-file-to-inject-before.js
new file mode 100644
index 000000000..6162e3d88
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/fake-file-to-inject-before.js
@@ -0,0 +1,5 @@
+// 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.
+
+console.log("Hello injection from local file in before.");
\ No newline at end of file
diff --git a/packages/tests/src/_jest/fixtures/fake-file-to-inject-middle.js b/packages/tests/src/_jest/fixtures/fake-file-to-inject-middle.js
new file mode 100644
index 000000000..99815a1c5
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/fake-file-to-inject-middle.js
@@ -0,0 +1,5 @@
+// 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.
+
+console.log("Hello injection from local file in middle.");
\ No newline at end of file
diff --git a/packages/tests/src/_jest/fixtures/project/main1.js b/packages/tests/src/_jest/fixtures/hard_project/main1.js
similarity index 91%
rename from packages/tests/src/_jest/fixtures/project/main1.js
rename to packages/tests/src/_jest/fixtures/hard_project/main1.js
index 2c2c0ff17..05793a717 100644
--- a/packages/tests/src/_jest/fixtures/project/main1.js
+++ b/packages/tests/src/_jest/fixtures/hard_project/main1.js
@@ -10,7 +10,7 @@ import fn2 from './workspaces/app/workspaceFile1.js';
 // Add a third party dependency.
 import * as chalk from 'chalk';
 
-console.log(chalk.cyan('Hello world!'));
+console.log(chalk.cyan('Hello World!'));
 
 fn();
 fn2();
diff --git a/packages/tests/src/_jest/fixtures/project/main2.js b/packages/tests/src/_jest/fixtures/hard_project/main2.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/project/main2.js
rename to packages/tests/src/_jest/fixtures/hard_project/main2.js
diff --git a/packages/tests/src/_jest/fixtures/hard_project/package.json b/packages/tests/src/_jest/fixtures/hard_project/package.json
new file mode 100644
index 000000000..ac25238f5
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/hard_project/package.json
@@ -0,0 +1,15 @@
+{
+    "name": "@tests/hard_project",
+    "private": true,
+    "license": "MIT",
+    "author": "Datadog",
+    "packageManager": "yarn@4.2.1",
+    "dependencies": {
+        "chalk": "2.3.1"
+    },
+    "devDependencies": {
+        "react": "19.0.0",
+        "react-dom": "19.0.0",
+        "react-router-dom": "6.28.0"
+    }
+}
diff --git a/packages/tests/src/_jest/fixtures/project/src/srcFile0.js b/packages/tests/src/_jest/fixtures/hard_project/src/srcFile0.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/project/src/srcFile0.js
rename to packages/tests/src/_jest/fixtures/hard_project/src/srcFile0.js
diff --git a/packages/tests/src/_jest/fixtures/project/src/srcFile1.js b/packages/tests/src/_jest/fixtures/hard_project/src/srcFile1.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/project/src/srcFile1.js
rename to packages/tests/src/_jest/fixtures/hard_project/src/srcFile1.js
diff --git a/packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile0.js b/packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile0.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile0.js
rename to packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile0.js
diff --git a/packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile1.js b/packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile1.js
similarity index 100%
rename from packages/tests/src/_jest/fixtures/project/workspaces/app/workspaceFile1.js
rename to packages/tests/src/_jest/fixtures/hard_project/workspaces/app/workspaceFile1.js
diff --git a/packages/tests/src/_jest/fixtures/project/package.json b/packages/tests/src/_jest/fixtures/package.json
similarity index 50%
rename from packages/tests/src/_jest/fixtures/project/package.json
rename to packages/tests/src/_jest/fixtures/package.json
index 77cc3b8b7..baafdfc02 100644
--- a/packages/tests/src/_jest/fixtures/project/package.json
+++ b/packages/tests/src/_jest/fixtures/package.json
@@ -1,10 +1,11 @@
 {
-    "name": "project",
+    "name": "@tests/fixtures",
     "private": true,
     "license": "MIT",
     "author": "Datadog",
     "packageManager": "yarn@4.2.1",
-    "dependencies": {
-        "chalk": "2.3.1"
-    }
+    "workspaces": [
+        "hard_project",
+        "easy_project"
+    ]
 }
diff --git a/packages/tests/src/_jest/fixtures/yarn.lock b/packages/tests/src/_jest/fixtures/yarn.lock
new file mode 100644
index 000000000..e8d79974f
--- /dev/null
+++ b/packages/tests/src/_jest/fixtures/yarn.lock
@@ -0,0 +1,149 @@
+# This file is generated by running "yarn install" inside your project.
+# Manual changes might be lost - proceed with caution!
+
+__metadata:
+  version: 8
+  cacheKey: 10
+
+"@remix-run/router@npm:1.21.0":
+  version: 1.21.0
+  resolution: "@remix-run/router@npm:1.21.0"
+  checksum: 10/cf0fb69d19c1b79095ff67c59cea89086f3982a9a54c8a993818a60fc76e0ebab5a8db647c1a96a662729fad8e806ddd0a96622adf473f5a9f0b99998b2dbad4
+  languageName: node
+  linkType: hard
+
+"@tests/easy_project@workspace:easy_project":
+  version: 0.0.0-use.local
+  resolution: "@tests/easy_project@workspace:easy_project"
+  dependencies:
+    chalk: "npm:2.3.1"
+    react: "npm:19.0.0"
+    react-dom: "npm:19.0.0"
+    react-router-dom: "npm:6.28.0"
+  languageName: unknown
+  linkType: soft
+
+"@tests/fixtures@workspace:.":
+  version: 0.0.0-use.local
+  resolution: "@tests/fixtures@workspace:."
+  languageName: unknown
+  linkType: soft
+
+"@tests/hard_project@workspace:hard_project":
+  version: 0.0.0-use.local
+  resolution: "@tests/hard_project@workspace:hard_project"
+  dependencies:
+    chalk: "npm:2.3.1"
+    react: "npm:19.0.0"
+    react-dom: "npm:19.0.0"
+    react-router-dom: "npm:6.28.0"
+  languageName: unknown
+  linkType: soft
+
+"ansi-styles@npm:^3.2.0":
+  version: 3.2.1
+  resolution: "ansi-styles@npm:3.2.1"
+  dependencies:
+    color-convert: "npm:^1.9.0"
+  checksum: 10/d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665
+  languageName: node
+  linkType: hard
+
+"chalk@npm:2.3.1":
+  version: 2.3.1
+  resolution: "chalk@npm:2.3.1"
+  dependencies:
+    ansi-styles: "npm:^3.2.0"
+    escape-string-regexp: "npm:^1.0.5"
+    supports-color: "npm:^5.2.0"
+  checksum: 10/53f7346b01d5bd93cceb1645bf3858ef4a211b4c69be152e391cdbe386038308e227c14f5518c4f437cbca72054f0593c19f3ebc75b042892c79f46b0605f60b
+  languageName: node
+  linkType: hard
+
+"color-convert@npm:^1.9.0":
+  version: 1.9.3
+  resolution: "color-convert@npm:1.9.3"
+  dependencies:
+    color-name: "npm:1.1.3"
+  checksum: 10/ffa319025045f2973919d155f25e7c00d08836b6b33ea2d205418c59bd63a665d713c52d9737a9e0fe467fb194b40fbef1d849bae80d674568ee220a31ef3d10
+  languageName: node
+  linkType: hard
+
+"color-name@npm:1.1.3":
+  version: 1.1.3
+  resolution: "color-name@npm:1.1.3"
+  checksum: 10/09c5d3e33d2105850153b14466501f2bfb30324a2f76568a408763a3b7433b0e50e5b4ab1947868e65cb101bb7cb75029553f2c333b6d4b8138a73fcc133d69d
+  languageName: node
+  linkType: hard
+
+"escape-string-regexp@npm:^1.0.5":
+  version: 1.0.5
+  resolution: "escape-string-regexp@npm:1.0.5"
+  checksum: 10/6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410
+  languageName: node
+  linkType: hard
+
+"has-flag@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "has-flag@npm:3.0.0"
+  checksum: 10/4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b
+  languageName: node
+  linkType: hard
+
+"react-dom@npm:19.0.0":
+  version: 19.0.0
+  resolution: "react-dom@npm:19.0.0"
+  dependencies:
+    scheduler: "npm:^0.25.0"
+  peerDependencies:
+    react: ^19.0.0
+  checksum: 10/aa64a2f1991042f516260e8b0eca0ae777b6c8f1aa2b5ae096e80bbb6ac9b005aef2bca697969841d34f7e1819556263476bdfea36c35092e8d9aefde3de2d9a
+  languageName: node
+  linkType: hard
+
+"react-router-dom@npm:6.28.0":
+  version: 6.28.0
+  resolution: "react-router-dom@npm:6.28.0"
+  dependencies:
+    "@remix-run/router": "npm:1.21.0"
+    react-router: "npm:6.28.0"
+  peerDependencies:
+    react: ">=16.8"
+    react-dom: ">=16.8"
+  checksum: 10/e637825132ea96c3514ef7b8322f9bf0b752a942d6b4ffc4c20e389b5911726adf3dba8208ed4b97bf5b9c3bd465d9d1a1db1a58a610a8d528f18d890e0b143f
+  languageName: node
+  linkType: hard
+
+"react-router@npm:6.28.0":
+  version: 6.28.0
+  resolution: "react-router@npm:6.28.0"
+  dependencies:
+    "@remix-run/router": "npm:1.21.0"
+  peerDependencies:
+    react: ">=16.8"
+  checksum: 10/f021a644513144884a567d9c2dcc432e8e3233f931378c219c5a3b5b842340f0faca86225a708bafca1e9010965afe1a7dada28aef5b7b6138c885c0552d9a7d
+  languageName: node
+  linkType: hard
+
+"react@npm:19.0.0":
+  version: 19.0.0
+  resolution: "react@npm:19.0.0"
+  checksum: 10/2490969c503f644703c88990d20e4011fa6119ddeca451e9de48f6d7ab058d670d2852a5fcd3aa3cd90a923ab2815d532637bd4a814add402ae5c0d4f129ee71
+  languageName: node
+  linkType: hard
+
+"scheduler@npm:^0.25.0":
+  version: 0.25.0
+  resolution: "scheduler@npm:0.25.0"
+  checksum: 10/e661e38503ab29a153429a99203fefa764f28b35c079719eb5efdd2c1c1086522f6653d8ffce388209682c23891a6d1d32fa6badf53c35fb5b9cd0c55ace42de
+  languageName: node
+  linkType: hard
+
+"supports-color@npm:^5.2.0":
+  version: 5.5.0
+  resolution: "supports-color@npm:5.5.0"
+  dependencies:
+    has-flag: "npm:^3.0.0"
+  checksum: 10/5f505c6fa3c6e05873b43af096ddeb22159831597649881aeb8572d6fe3b81e798cc10840d0c9735e0026b250368851b7f77b65e84f4e4daa820a4f69947f55b
+  languageName: node
+  linkType: hard
diff --git a/packages/tests/src/_jest/globalSetup.ts b/packages/tests/src/_jest/globalSetup.ts
index e87dd784a..81fd24ca6 100644
--- a/packages/tests/src/_jest/globalSetup.ts
+++ b/packages/tests/src/_jest/globalSetup.ts
@@ -2,11 +2,89 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { logTips } from './helpers/tips';
+import chalk from 'chalk';
+import { execFileSync } from 'child_process';
+import type { ExecFileSyncOptionsWithStringEncoding } from 'child_process';
+import path from 'path';
+
+import { getEnv, logEnv, setupEnv } from './helpers/env';
+
+const c = chalk.bold.dim;
+
+const setupGit = (execOptions: ExecFileSyncOptionsWithStringEncoding) => {
+    const setupSteps: { name: string; commands: string[]; fallbacks?: string[] }[] = [
+        {
+            // Initialize a git repository.
+            name: 'Init',
+            commands: ['git init'],
+        },
+        {
+            // Ensure we have a local user.
+            name: 'Git user',
+            commands: ['git config --local user.email'],
+            fallbacks: [
+                'git config --local user.email fake@example.com',
+                'git config --local user.name fakeuser',
+            ],
+        },
+        {
+            // Ensure origin exists
+            name: 'Origin',
+            commands: ['git ls-remote --get-url'],
+            fallbacks: ['git remote add origin fake_origin'],
+        },
+        {
+            // Ensure HEAD exists
+            name: 'HEAD',
+            commands: ['git rev-parse --verify HEAD'],
+            // Fake HEAD.
+            fallbacks: ['git commit --allow-empty -n -m "abc"'],
+        },
+    ];
+
+    const runCmds = (commands: string[]) => {
+        for (const command of commands) {
+            const args = command.split(' ');
+            execFileSync(args[0], args.slice(1), execOptions);
+        }
+    };
+    for (const { name, commands, fallbacks } of setupSteps) {
+        try {
+            runCmds(commands);
+        } catch (e) {
+            if (!fallbacks || fallbacks.length === 0) {
+                throw e;
+            }
+            console.log(c.yellow(`  - ${name} does not exist, creating it.`));
+            runCmds(fallbacks);
+        }
+    }
+};
 
 const globalSetup = () => {
+    const timeId = `[${c.cyan('Test environment setup duration')}]`;
+    console.time(timeId);
+    const env = getEnv(process.argv);
+    // Setup the environment.
+    setupEnv(env);
     // Log some tips to the console.
-    logTips();
+    logEnv(env);
+
+    // Setup fixtures.
+    const execOptions: ExecFileSyncOptionsWithStringEncoding = {
+        cwd: path.resolve(__dirname, './fixtures'),
+        encoding: 'utf-8',
+        stdio: [],
+    };
+
+    try {
+        // Install dependencies.
+        execFileSync('yarn', ['install'], execOptions);
+        setupGit(execOptions);
+    } catch (e) {
+        console.error('Fixtures setup failed:', e);
+    }
+    console.timeEnd(timeId);
 };
 
 export default globalSetup;
diff --git a/packages/tests/src/_jest/helpers/configBundlers.ts b/packages/tests/src/_jest/helpers/configBundlers.ts
index 3de7b2368..31d65d197 100644
--- a/packages/tests/src/_jest/helpers/configBundlers.ts
+++ b/packages/tests/src/_jest/helpers/configBundlers.ts
@@ -7,43 +7,56 @@ import { datadogRollupPlugin } from '@datadog/rollup-plugin';
 import { datadogRspackPlugin } from '@datadog/rspack-plugin';
 import { datadogVitePlugin } from '@datadog/vite-plugin';
 import type { Options } from '@dd/core/types';
-import commonjs from '@rollup/plugin-commonjs';
-import { nodeResolve } from '@rollup/plugin-node-resolve';
+import {
+    configEsbuild,
+    configRollup,
+    configRspack,
+    configVite,
+    configWebpack4,
+    configWebpack5,
+} from '@dd/tools/bundlers';
 import type { RspackOptions } from '@rspack/core';
 import type { BuildOptions } from 'esbuild';
 import path from 'path';
 import type { RollupOptions } from 'rollup';
 import type { UserConfig } from 'vite';
-import type { Configuration as Configuration4, Plugin } from 'webpack4';
+import type { Configuration as Configuration4 } from 'webpack4';
 import webpack4 from 'webpack4';
 import type { Configuration } from 'webpack5';
 import webpack5 from 'webpack5';
 
-import { defaultDestination, defaultEntry, defaultPluginOptions } from './mocks';
-import type { BundlerOverrides } from './types';
-import { getBaseXpackConfig, getWebpack4Entries, getWebpackPlugin } from './xpackConfigs';
+import { getOutDir } from './env';
+import { getWebpackPlugin } from './getWebpackPlugin';
+import { defaultEntry, defaultPluginOptions } from './mocks';
+import type { BundlerOptionsOverrides } from './types';
 
 export const getRspackOptions = (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: BundlerOverrides['rspack'] = {},
+    bundlerOverrides: BundlerOptionsOverrides['rspack'] = {},
 ): RspackOptions => {
     const newPluginOptions = {
         ...defaultPluginOptions,
         ...pluginOverrides,
     };
 
+    const plugin = datadogRspackPlugin(newPluginOptions);
+
     return {
-        ...(getBaseXpackConfig(seed, 'rspack') as RspackOptions),
-        plugins: [datadogRspackPlugin(newPluginOptions)],
+        ...configRspack({
+            workingDir,
+            entry: { main: path.resolve(workingDir, defaultEntry) },
+            outDir: getOutDir(workingDir, 'rspack'),
+            plugins: [plugin],
+        }),
         ...bundlerOverrides,
     };
 };
 
 export const getWebpack5Options = (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: BundlerOverrides['webpack5'] = {},
+    bundlerOverrides: BundlerOptionsOverrides['webpack5'] = {},
 ): Configuration => {
     const newPluginOptions = {
         ...defaultPluginOptions,
@@ -53,16 +66,20 @@ export const getWebpack5Options = (
     const plugin = getWebpackPlugin(newPluginOptions, webpack5);
 
     return {
-        ...getBaseXpackConfig(seed, 'webpack5'),
-        plugins: [plugin],
+        ...configWebpack5({
+            workingDir,
+            entry: { main: path.resolve(workingDir, defaultEntry) },
+            outDir: getOutDir(workingDir, 'webpack5'),
+            plugins: [plugin],
+        }),
         ...bundlerOverrides,
     };
 };
 
 export const getWebpack4Options = (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: BundlerOverrides['webpack4'] = {},
+    bundlerOverrides: BundlerOptionsOverrides['webpack4'] = {},
 ): Configuration4 => {
     const newPluginOptions = {
         ...defaultPluginOptions,
@@ -72,18 +89,21 @@ export const getWebpack4Options = (
     const plugin = getWebpackPlugin(newPluginOptions, webpack4);
 
     return {
-        ...getBaseXpackConfig(seed, 'webpack4'),
-        entry: getWebpack4Entries(defaultEntry),
-        plugins: [plugin as unknown as Plugin],
+        ...configWebpack4({
+            workingDir,
+            entry: { main: path.resolve(workingDir, defaultEntry) },
+            outDir: getOutDir(workingDir, 'webpack4'),
+            plugins: [plugin],
+        }),
         node: false,
         ...bundlerOverrides,
     };
 };
 
 export const getEsbuildOptions = (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: BundlerOverrides['esbuild'] = {},
+    bundlerOverrides: BundlerOptionsOverrides['esbuild'] = {},
 ): BuildOptions => {
     const newPluginOptions = {
         ...defaultPluginOptions,
@@ -91,59 +111,37 @@ export const getEsbuildOptions = (
     };
 
     return {
-        bundle: true,
-        chunkNames: 'chunk.[hash]',
-        entryPoints: { main: defaultEntry },
-        entryNames: '[name]',
+        ...configEsbuild({
+            workingDir,
+            entry: { main: defaultEntry },
+            outDir: getOutDir(workingDir, 'esbuild'),
+            plugins: [datadogEsbuildPlugin(newPluginOptions)],
+        }),
         format: 'esm',
-        outdir: path.join(defaultDestination, seed, 'esbuild'),
-        plugins: [datadogEsbuildPlugin(newPluginOptions)],
-        sourcemap: true,
         splitting: true,
         ...bundlerOverrides,
     };
 };
 
-export const getRollupBaseConfig = (seed: string, bundlerName: string): RollupOptions => {
-    return {
-        input: defaultEntry,
-        onwarn: (warning, handler) => {
-            if (
-                !/Circular dependency:/.test(warning.message) &&
-                !/Sourcemap is likely to be incorrect/.test(warning.message)
-            ) {
-                return handler(warning);
-            }
-        },
-        output: {
-            chunkFileNames: 'chunk.[hash].js',
-            compact: false,
-            dir: path.join(defaultDestination, seed, bundlerName),
-            entryFileNames: '[name].js',
-            sourcemap: true,
-        },
-    };
-};
-
 export const getRollupOptions = (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: BundlerOverrides['rollup'] = {},
+    bundlerOverrides: BundlerOptionsOverrides['rollup'] = {},
 ): RollupOptions => {
     const newPluginOptions = {
         ...defaultPluginOptions,
         ...pluginOverrides,
     };
 
-    const baseConfig = getRollupBaseConfig(seed, 'rollup');
+    const baseConfig = configRollup({
+        workingDir,
+        entry: { main: defaultEntry },
+        outDir: getOutDir(workingDir, 'rollup'),
+        plugins: [datadogRollupPlugin(newPluginOptions)],
+    });
 
     return {
         ...baseConfig,
-        plugins: [
-            commonjs(),
-            datadogRollupPlugin(newPluginOptions),
-            nodeResolve({ preferBuiltins: true, browser: true }),
-        ],
         ...bundlerOverrides,
         output: {
             ...baseConfig.output,
@@ -153,31 +151,35 @@ export const getRollupOptions = (
 };
 
 export const getViteOptions = (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: BundlerOverrides['vite'] = {},
+    bundlerOverrides: BundlerOptionsOverrides['vite'] = {},
 ): UserConfig => {
     const newPluginOptions = {
         ...defaultPluginOptions,
         ...pluginOverrides,
     };
 
-    const baseConfig = getRollupBaseConfig(seed, 'vite');
+    const baseConfig = configVite({
+        workingDir,
+        entry: { main: defaultEntry },
+        outDir: getOutDir(workingDir, 'vite'),
+        plugins: [datadogVitePlugin(newPluginOptions)],
+    });
 
     return {
+        root: workingDir,
+        ...baseConfig,
         build: {
-            assetsDir: '', // Disable assets dir to simplify the test.
-            minify: false,
+            ...baseConfig.build,
             rollupOptions: {
-                ...baseConfig,
+                ...baseConfig.build?.rollupOptions,
                 ...bundlerOverrides,
                 output: {
-                    ...baseConfig.output,
+                    ...baseConfig.build?.rollupOptions?.output,
                     ...bundlerOverrides.output,
                 },
             },
         },
-        logLevel: 'silent',
-        plugins: [datadogVitePlugin(newPluginOptions)],
     };
 };
diff --git a/packages/tests/src/_jest/helpers/constants.ts b/packages/tests/src/_jest/helpers/constants.ts
index 0e70787be..345b43b73 100644
--- a/packages/tests/src/_jest/helpers/constants.ts
+++ b/packages/tests/src/_jest/helpers/constants.ts
@@ -20,17 +20,3 @@ export const BUNDLER_VERSIONS: Record<BundlerFullName, string> = {
     webpack4: require('webpack4').version,
     webpack5: require('webpack5').version,
 };
-
-// Handle --cleanup flag.
-export const NO_CLEANUP = process.argv.includes('--cleanup=0');
-
-// Handle --build flag.
-export const NEED_BUILD = process.argv.includes('--build=1');
-
-// Handle --bundlers flag.
-export const REQUESTED_BUNDLERS = process.argv.includes('--bundlers')
-    ? process.argv[process.argv.indexOf('--bundlers') + 1].split(',')
-    : process.argv
-          .find((arg) => arg.startsWith('--bundlers='))
-          ?.split('=')[1]
-          .split(',') ?? [];
diff --git a/packages/tests/src/_jest/helpers/env.ts b/packages/tests/src/_jest/helpers/env.ts
new file mode 100644
index 000000000..6f87a3c9b
--- /dev/null
+++ b/packages/tests/src/_jest/helpers/env.ts
@@ -0,0 +1,133 @@
+// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants';
+import { mkdir } from '@dd/core/helpers';
+import type { BundlerFullName } from '@dd/core/types';
+import { bgYellow, dim, green, red } from '@dd/tools/helpers';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+
+const fsp = fs.promises;
+
+type TestEnv = {
+    NO_CLEANUP: boolean;
+    NEED_BUILD: boolean;
+    REQUESTED_BUNDLERS: string[];
+};
+
+export const getEnv = (argv: string[]): TestEnv => {
+    // Handle --cleanup flag.
+    const NO_CLEANUP = argv.includes('--cleanup=0');
+
+    // Handle --build flag.
+    const NEED_BUILD = argv.includes('--build=1');
+
+    // Handle --bundlers flag.
+    const REQUESTED_BUNDLERS = argv.includes('--bundlers')
+        ? argv[argv.indexOf('--bundlers') + 1].split(',')
+        : argv
+              .find((arg) => arg.startsWith('--bundlers='))
+              ?.split('=')[1]
+              .split(',') ?? [];
+
+    return {
+        NO_CLEANUP,
+        NEED_BUILD,
+        REQUESTED_BUNDLERS,
+    };
+};
+
+export const setupEnv = (env: TestEnv): void => {
+    const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = env;
+
+    if (NO_CLEANUP) {
+        process.env.NO_CLEANUP = '1';
+    }
+
+    if (NEED_BUILD) {
+        process.env.NEED_BUILD = '1';
+    }
+
+    if (REQUESTED_BUNDLERS.length) {
+        process.env.REQUESTED_BUNDLERS = REQUESTED_BUNDLERS.join(',');
+    }
+};
+
+export const logEnv = (env: TestEnv) => {
+    const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = env;
+    const envLogs = [];
+    if (NO_CLEANUP) {
+        envLogs.push(bgYellow(" Won't clean up "));
+    }
+
+    if (NEED_BUILD) {
+        envLogs.push(bgYellow(' Will also build used plugins '));
+    }
+
+    if (REQUESTED_BUNDLERS.length) {
+        if (
+            !(REQUESTED_BUNDLERS as BundlerFullName[]).every((bundler) =>
+                FULL_NAME_BUNDLERS.includes(bundler),
+            )
+        ) {
+            throw new Error(
+                `Invalid "${red(`--bundlers ${REQUESTED_BUNDLERS.join(',')}`)}".\nValid bundlers are ${FULL_NAME_BUNDLERS.map(
+                    (b) => green(b),
+                ).join(', ')}.`,
+            );
+        }
+        const bundlersList = REQUESTED_BUNDLERS.map((bundler) => green(bundler)).join(', ');
+        envLogs.push(`Running ${bgYellow(' ONLY ')} for ${bundlersList}.`);
+    }
+
+    if (!NO_CLEANUP || !NEED_BUILD || REQUESTED_BUNDLERS.length) {
+        const tips: string[] = [];
+        if (!NO_CLEANUP) {
+            tips.push(`  ${green('--cleanup=0')} to keep the built artifacts.`);
+        }
+        if (!NEED_BUILD) {
+            tips.push(`  ${green('--build=1')} to force the build of the used plugins.`);
+        }
+        if (!REQUESTED_BUNDLERS.length) {
+            tips.push(`  ${green('--bundlers=webpack4,esbuild')} to only use specified bundlers.`);
+        }
+        envLogs.push(dim(`\nYou can also use : \n${tips.join('\n')}\n`));
+    }
+
+    if (envLogs.length) {
+        console.log(`\n${envLogs.join('\n')}\n`);
+    }
+};
+
+export const getOutDir = (workingDir: string, folderName: string): string => {
+    return path.resolve(workingDir, `./dist/${folderName}`);
+};
+
+const FIXTURE_DIR = path.resolve(__dirname, '../fixtures');
+export const prepareWorkingDir = async (seed: string) => {
+    const timeId = `[${dim.cyan('Preparing working directory duration')}]`;
+    console.time(timeId);
+    const tmpDir = os.tmpdir();
+    const workingDir = path.resolve(tmpDir, seed);
+
+    // Create the directory.
+    await mkdir(workingDir);
+
+    // Need to use realpathSync to avoid issues with symlinks on macos (prefix with /private).
+    // cf: https://github.com/nodejs/node/issues/11422
+    const realWorkingDir = await fsp.realpath(workingDir);
+
+    // Copy mock projects into it.
+    await fsp.cp(`${FIXTURE_DIR}/`, `${realWorkingDir}/`, {
+        recursive: true,
+        errorOnExist: true,
+        force: true,
+    });
+
+    console.timeEnd(timeId);
+
+    return realWorkingDir;
+};
diff --git a/packages/tests/src/_jest/helpers/getWebpackPlugin.ts b/packages/tests/src/_jest/helpers/getWebpackPlugin.ts
new file mode 100644
index 000000000..e54cca53e
--- /dev/null
+++ b/packages/tests/src/_jest/helpers/getWebpackPlugin.ts
@@ -0,0 +1,22 @@
+// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/).
+// Copyright 2019-Present Datadog, Inc.
+
+import type { Options } from '@dd/core/types';
+import { buildPluginFactory } from '@dd/factory';
+import type webpack4 from 'webpack4';
+import type webpack5 from 'webpack5';
+
+import { PLUGIN_VERSIONS } from './constants';
+
+// Return the correct plugin for webpack 4 or 5.
+export const getWebpackPlugin = (
+    pluginOptions: Options,
+    bundler: typeof webpack4 | typeof webpack5,
+) => {
+    // Need to use the factory directly since we pass the bundler to the factory.
+    return buildPluginFactory({
+        bundler,
+        version: PLUGIN_VERSIONS.webpack,
+    }).webpack(pluginOptions);
+};
diff --git a/packages/tests/src/_jest/helpers/mocks.ts b/packages/tests/src/_jest/helpers/mocks.ts
index fb71e71e8..520ff73e9 100644
--- a/packages/tests/src/_jest/helpers/mocks.ts
+++ b/packages/tests/src/_jest/helpers/mocks.ts
@@ -2,46 +2,37 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { outputJsonSync } from '@dd/core/helpers';
 import type {
+    BuildReport,
     File,
-    GetCustomPlugins,
     GetPluginsOptions,
     GlobalContext,
-    IterableElement,
     Logger,
     LogLevel,
     Options,
 } from '@dd/core/types';
-import { serializeBuildReport } from '@dd/internal-build-report-plugin/helpers';
-import { getSourcemapsConfiguration } from '@dd/tests/plugins/rum/testHelpers';
-import { getTelemetryConfiguration } from '@dd/tests/plugins/telemetry/testHelpers';
+import { getAbsolutePath } from '@dd/internal-build-report-plugin/helpers';
+import { getSourcemapsConfiguration } from '@dd/tests/unit/plugins/error-tracking/testHelpers';
+import { getTelemetryConfiguration } from '@dd/tests/unit/plugins/telemetry/testHelpers';
+import { configXpack } from '@dd/tools/bundlers';
+import type { PluginBuild } from 'esbuild';
 import path from 'path';
-import type { Configuration as Configuration4 } from 'webpack4';
 
-import type { BundlerOverrides } from './types';
-import { getBaseXpackConfig, getWebpack4Entries } from './xpackConfigs';
-
-if (!process.env.PROJECT_CWD) {
-    throw new Error('Please update the usage of `process.env.PROJECT_CWD`.');
-}
-const ROOT = process.env.PROJECT_CWD!;
+import type { BundlerOptionsOverrides, BundlerOverrides } from './types';
 
 export const FAKE_URL = 'https://example.com';
 export const API_PATH = '/v2/srcmap';
 export const INTAKE_URL = `${FAKE_URL}${API_PATH}`;
 
-export const defaultEntry = '@dd/tests/_jest/fixtures/main.js';
+export const defaultEntry = './easy_project/main.js';
 export const defaultEntries = {
-    app1: '@dd/tests/_jest/fixtures/project/main1.js',
-    app2: '@dd/tests/_jest/fixtures/project/main2.js',
+    app1: './hard_project/main1.js',
+    app2: './hard_project/main2.js',
 };
-export const defaultDestination = path.resolve(ROOT, 'packages/tests/src/_jest/fixtures/dist');
 
+export const defaultAuth = { apiKey: '123', appKey: '123' };
 export const defaultPluginOptions: GetPluginsOptions = {
-    auth: {
-        apiKey: '123',
-    },
+    auth: defaultAuth,
     disableGit: false,
     logLevel: 'debug',
 };
@@ -64,78 +55,139 @@ const logFn: Logger = {
 };
 export const mockLogger: Logger = logFn;
 
-export const getContextMock = (options: Partial<GlobalContext> = {}): GlobalContext => {
+export const getEsbuildMock = (overrides: Partial<PluginBuild> = {}): PluginBuild => {
+    return {
+        resolve: async (filepath) => {
+            return {
+                errors: [],
+                warnings: [],
+                external: false,
+                sideEffects: false,
+                namespace: '',
+                suffix: '',
+                pluginData: {},
+                path: getAbsolutePath(process.cwd(), filepath),
+            };
+        },
+        onStart: jest.fn(),
+        onEnd: jest.fn(),
+        onResolve: jest.fn(),
+        onLoad: jest.fn(),
+        onDispose: jest.fn(),
+        ...overrides,
+        esbuild: {
+            context: jest.fn(),
+            build: jest.fn(),
+            buildSync: jest.fn(),
+            transform: jest.fn(),
+            transformSync: jest.fn(),
+            formatMessages: jest.fn(),
+            formatMessagesSync: jest.fn(),
+            analyzeMetafile: jest.fn(),
+            analyzeMetafileSync: jest.fn(),
+            initialize: jest.fn(),
+            version: '1.0.0',
+            ...(overrides.esbuild || {}),
+        },
+        initialOptions: {
+            ...(overrides.initialOptions || {}),
+        },
+    };
+};
+
+export const getMockBuild = (overrides: Partial<BuildReport> = {}): BuildReport => ({
+    errors: [],
+    warnings: [],
+    logs: [],
+    ...overrides,
+    bundler: {
+        name: 'esbuild',
+        fullName: 'esbuild',
+        version: 'FAKE_VERSION',
+        ...(overrides.bundler || {}),
+    },
+});
+
+export const getContextMock = (overrides: Partial<GlobalContext> = {}): GlobalContext => {
     return {
-        auth: { apiKey: 'FAKE_API_KEY' },
+        auth: defaultAuth,
         bundler: {
-            name: 'esbuild',
-            fullName: 'esbuild',
+            ...getMockBuild().bundler,
             outDir: '/cwd/path',
-            version: 'FAKE_VERSION',
-        },
-        build: {
-            warnings: [],
-            errors: [],
-            logs: [],
         },
+        build: getMockBuild(),
         cwd: '/cwd/path',
         inject: jest.fn(),
         pluginNames: [],
         start: Date.now(),
         version: 'FAKE_VERSION',
-        ...options,
+        ...overrides,
     };
 };
 
-export const getComplexBuildOverrides = (
-    overrides: BundlerOverrides = {},
-): Required<BundlerOverrides> => {
-    const bundlerOverrides = {
-        rollup: {
-            input: defaultEntries,
-            ...overrides.rollup,
-        },
-        vite: {
-            input: defaultEntries,
-            ...overrides.vite,
-        },
-        esbuild: {
-            entryPoints: defaultEntries,
-            ...overrides.esbuild,
-        },
-        rspack: { entry: defaultEntries, ...overrides.rspack },
-        webpack5: { entry: defaultEntries, ...overrides.webpack5 },
-        webpack4: {
-            entry: getWebpack4Entries(defaultEntries),
-            ...overrides.webpack4,
-        },
-    };
+export const getComplexBuildOverrides =
+    (overrides?: BundlerOverrides) =>
+    (workingDir: string): Required<BundlerOverrides> => {
+        const overridesResolved =
+            typeof overrides === 'function' ? overrides(workingDir) : overrides || {};
+
+        // Using a function to avoid mutation of the same object later down the line.
+        const entries = () =>
+            Object.fromEntries(
+                Object.entries(defaultEntries).map(([key, value]) => [
+                    key,
+                    path.resolve(workingDir, value),
+                ]),
+            );
 
-    return bundlerOverrides;
-};
+        const bundlerOverrides = {
+            rollup: {
+                input: entries(),
+                ...overridesResolved.rollup,
+            },
+            vite: {
+                input: entries(),
+                ...overridesResolved.vite,
+            },
+            esbuild: {
+                entryPoints: entries(),
+                ...overridesResolved.esbuild,
+            },
+            rspack: { entry: entries(), ...overridesResolved.rspack },
+            webpack5: { entry: entries(), ...overridesResolved.webpack5 },
+            webpack4: { entry: entries(), ...overridesResolved.webpack4 },
+        };
+
+        return bundlerOverrides;
+    };
 
 // To get a node safe build.
 export const getNodeSafeBuildOverrides = (
-    overrides: BundlerOverrides = {},
-): Required<BundlerOverrides> => {
+    workingDir: string,
+    overrides?: BundlerOverrides,
+): Required<BundlerOptionsOverrides> => {
+    const overridesResolved =
+        typeof overrides === 'function' ? overrides(workingDir) : overrides || {};
     // We don't care about the seed and the bundler name
     // as we won't use the output config here.
-    const baseWebpack = getBaseXpackConfig('fake_seed', 'fake_bundler');
-    const bundlerOverrides: Required<BundlerOverrides> = {
+    const baseWebpack = configXpack({ workingDir: 'fake_cwd', outDir: 'dist', entry: {} });
+    const bundlerOverrides: Required<BundlerOptionsOverrides> = {
         rollup: {
+            ...overridesResolved.rollup,
             output: {
+                ...overridesResolved.rollup?.output,
                 format: 'cjs',
             },
-            ...overrides.rollup,
         },
         vite: {
+            ...overridesResolved.vite,
             output: {
+                ...overridesResolved.vite?.output,
                 format: 'cjs',
             },
-            ...overrides.vite,
         },
         esbuild: {
-            ...overrides.esbuild,
+            ...overridesResolved.esbuild,
         },
         rspack: {
             target: 'node',
@@ -143,7 +195,7 @@ export const getNodeSafeBuildOverrides = (
                 ...baseWebpack.optimization,
                 splitChunks: false,
             },
-            ...overrides.rspack,
+            ...overridesResolved.rspack,
         },
         webpack5: {
             target: 'node',
@@ -151,15 +203,15 @@ export const getNodeSafeBuildOverrides = (
                 ...baseWebpack.optimization,
                 splitChunks: false,
             },
-            ...overrides.webpack5,
+            ...overridesResolved.webpack5,
         },
         webpack4: {
             target: 'node',
             optimization: {
-                ...(baseWebpack.optimization as Configuration4['optimization']),
+                ...baseWebpack.optimization,
                 splitChunks: false,
             },
-            ...overrides.webpack4,
+            ...overridesResolved.webpack4,
         },
     };
 
@@ -170,7 +222,7 @@ export const getNodeSafeBuildOverrides = (
 export const getFullPluginConfig = (overrides: Partial<Options> = {}): Options => {
     return {
         ...defaultPluginOptions,
-        rum: {
+        errorTracking: {
             sourcemaps: getSourcemapsConfiguration(),
         },
         telemetry: getTelemetryConfiguration(),
@@ -189,82 +241,6 @@ export const getMirroredFixtures = (paths: string[], cwd: string) => {
     return fixtures;
 };
 
-// Returns a customPlugin to output some debug files.
-type CustomPlugins = ReturnType<GetCustomPlugins>;
-export const debugFilesPlugins = (context: GlobalContext): CustomPlugins => {
-    const rollupPlugin: IterableElement<CustomPlugins>['rollup'] = {
-        writeBundle(options, bundle) {
-            outputJsonSync(
-                path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`),
-                bundle,
-            );
-        },
-    };
-
-    const xpackPlugin: IterableElement<CustomPlugins>['webpack'] &
-        IterableElement<CustomPlugins>['rspack'] = (compiler) => {
-        type Compilation = Parameters<Parameters<typeof compiler.hooks.afterEmit.tap>[1]>[0];
-
-        compiler.hooks.afterEmit.tap('bundler-outputs', (compilation: Compilation) => {
-            const stats = compilation.getStats().toJson({
-                all: false,
-                assets: true,
-                children: true,
-                chunks: true,
-                chunkGroupAuxiliary: true,
-                chunkGroupChildren: true,
-                chunkGroups: true,
-                chunkModules: true,
-                chunkRelations: true,
-                entrypoints: true,
-                errors: true,
-                ids: true,
-                modules: true,
-                nestedModules: true,
-                reasons: true,
-                relatedAssets: true,
-                warnings: true,
-            });
-            outputJsonSync(
-                path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`),
-                stats,
-            );
-        });
-    };
-
-    return [
-        {
-            name: 'build-report',
-            writeBundle() {
-                outputJsonSync(
-                    path.resolve(context.bundler.outDir, `report.${context.bundler.fullName}.json`),
-                    serializeBuildReport(context.build),
-                );
-            },
-        },
-        {
-            name: 'bundler-outputs',
-            esbuild: {
-                setup(build) {
-                    build.onEnd((result) => {
-                        outputJsonSync(
-                            path.resolve(
-                                context.bundler.outDir,
-                                `output.${context.bundler.fullName}.json`,
-                            ),
-                            result.metafile,
-                        );
-                    });
-                },
-            },
-            rspack: xpackPlugin,
-            rollup: rollupPlugin,
-            vite: rollupPlugin,
-            webpack: xpackPlugin,
-        },
-    ];
-};
-
 // Filter out stuff from the build report.
 export const filterOutParticularities = (input: File) =>
     // Vite injects its own preloader helper.
@@ -274,4 +250,4 @@ export const filterOutParticularities = (input: File) =>
     // Exclude webpack buildin modules, which are webpack internal dependencies.
     !input.filepath.includes('webpack4/buildin') &&
     // Exclude webpack's fake entry point.
-    !input.filepath.includes('fixtures/project/empty.js');
+    !input.filepath.includes('fixtures/empty.js');
diff --git a/packages/tests/src/_jest/helpers/runBundlers.ts b/packages/tests/src/_jest/helpers/runBundlers.ts
index ada0b3d96..0d74a6958 100644
--- a/packages/tests/src/_jest/helpers/runBundlers.ts
+++ b/packages/tests/src/_jest/helpers/runBundlers.ts
@@ -2,15 +2,22 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { rm } from '@dd/core/helpers';
+import { getUniqueId, rm } from '@dd/core/helpers';
 import type { Options } from '@dd/core/types';
-import { executeSync, green } from '@dd/tools/helpers';
-import type { RspackOptions, Stats as RspackStats } from '@rspack/core';
+import {
+    buildWithEsbuild,
+    buildWithRollup,
+    buildWithRspack,
+    buildWithVite,
+    buildWithWebpack4,
+    buildWithWebpack5,
+} from '@dd/tools/bundlers';
+import { buildPlugins, green } from '@dd/tools/helpers';
+import type { RspackOptions } from '@rspack/core';
 import type { BuildOptions } from 'esbuild';
-import path from 'path';
 import type { RollupOptions } from 'rollup';
-import type { Configuration as Configuration4, Stats as Stats4 } from 'webpack4';
-import type { Configuration, Stats } from 'webpack5';
+import type { Configuration as Configuration4 } from 'webpack4';
+import type { Configuration } from 'webpack5';
 
 import {
     getEsbuildOptions,
@@ -20,45 +27,18 @@ import {
     getWebpack4Options,
     getWebpack5Options,
 } from './configBundlers';
-import { NEED_BUILD, NO_CLEANUP, PLUGIN_VERSIONS, REQUESTED_BUNDLERS } from './constants';
-import { defaultDestination } from './mocks';
-import type { Bundler, BundlerRunFunction, CleanupFn } from './types';
-
-const xpackCallback = (
-    err: Error | null,
-    stats: Stats4 | Stats | RspackStats | undefined,
-    resolve: (value: unknown) => void,
-    reject: (reason?: any) => void,
-    delay: number = 0,
-) => {
-    if (err) {
-        reject(err);
-        return;
-    }
-
-    if (!stats) {
-        reject('No stats returned from webpack.');
-        return;
-    }
-
-    const { errors, warnings } = stats.compilation;
-    if (errors?.length) {
-        reject(errors[0]);
-        return;
-    }
-
-    if (warnings?.length) {
-        console.warn(warnings.join('\n'));
-    }
-
-    // Delay the resolve to give time to the bundler to finish writing the files.
-    // Webpack4 in particular is impacted by this and otherwise triggers a
-    // "Jest did not exit one second after the test run has completed." warning.
-    // TODO: Investigate this need for a delay after webpack 4's build.
-    setTimeout(() => {
-        resolve(stats);
-    }, delay);
-};
+import { PLUGIN_VERSIONS } from './constants';
+import { prepareWorkingDir } from './env';
+import type {
+    Bundler,
+    BundlerRunFunction,
+    CleanupFn,
+    BundlerOverrides,
+    CleanupEverythingFn,
+} from './types';
+
+// Get the environment variables.
+const { NO_CLEANUP, NEED_BUILD, REQUESTED_BUNDLERS } = process.env;
 
 const getCleanupFunction =
     (bundlerName: string, outdirs: (string | undefined)[]): CleanupFn =>
@@ -82,107 +62,52 @@ const getCleanupFunction =
     };
 
 export const runRspack: BundlerRunFunction = async (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Options = {},
     bundlerOverrides: Partial<RspackOptions> = {},
 ) => {
-    const bundlerConfigs = getRspackOptions(seed, pluginOverrides, bundlerOverrides);
-    const { rspack } = await import('@rspack/core');
-    const errors = [];
-
-    try {
-        await new Promise((resolve, reject) => {
-            rspack(bundlerConfigs, (err, stats) => {
-                xpackCallback(err, stats, resolve, reject);
-            });
-        });
-    } catch (e: any) {
-        console.error(`Build failed for Rspack`, e);
-        errors.push(`[RSPACK] : ${e.message}`);
-    }
-
+    const bundlerConfigs = getRspackOptions(workingDir, pluginOverrides, bundlerOverrides);
+    const { errors } = await buildWithRspack(bundlerConfigs);
     return { cleanup: getCleanupFunction('Rspack', [bundlerConfigs.output?.path]), errors };
 };
 
 export const runWebpack5: BundlerRunFunction = async (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Options = {},
     bundlerOverrides: Partial<Configuration> = {},
 ) => {
-    const bundlerConfigs = getWebpack5Options(seed, pluginOverrides, bundlerOverrides);
-    const { webpack } = await import('webpack5');
-    const errors = [];
-
-    try {
-        await new Promise((resolve, reject) => {
-            webpack(bundlerConfigs, (err, stats) => {
-                xpackCallback(err, stats, resolve, reject);
-            });
-        });
-    } catch (e: any) {
-        console.error(`Build failed for Webpack 5`, e);
-        errors.push(`[WEBPACK5] : ${e.message}`);
-    }
-
+    const bundlerConfigs = getWebpack5Options(workingDir, pluginOverrides, bundlerOverrides);
+    const { errors } = await buildWithWebpack5(bundlerConfigs);
     return { cleanup: getCleanupFunction('Webpack 5', [bundlerConfigs.output?.path]), errors };
 };
 
 export const runWebpack4: BundlerRunFunction = async (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Options = {},
     bundlerOverrides: Partial<Configuration4> = {},
 ) => {
-    const bundlerConfigs = getWebpack4Options(seed, pluginOverrides, bundlerOverrides);
-    const webpack = (await import('webpack4')).default;
-    const errors = [];
-
-    try {
-        await new Promise((resolve, reject) => {
-            webpack(bundlerConfigs, (err, stats) => {
-                xpackCallback(err, stats, resolve, reject, 600);
-            });
-        });
-    } catch (e: any) {
-        console.error(`Build failed for Webpack 4`, e);
-        errors.push(`[WEBPACK4] : ${e.message}`);
-    }
-
+    const bundlerConfigs = getWebpack4Options(workingDir, pluginOverrides, bundlerOverrides);
+    const { errors } = await buildWithWebpack4(bundlerConfigs);
     return { cleanup: getCleanupFunction('Webpack 4', [bundlerConfigs.output?.path]), errors };
 };
 
 export const runEsbuild: BundlerRunFunction = async (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Options = {},
     bundlerOverrides: Partial<BuildOptions> = {},
 ) => {
-    const bundlerConfigs = getEsbuildOptions(seed, pluginOverrides, bundlerOverrides);
-    const { build } = await import('esbuild');
-    const errors = [];
-
-    try {
-        await build(bundlerConfigs);
-    } catch (e: any) {
-        console.error(`Build failed for ESBuild`, e);
-        errors.push(`[ESBUILD] : ${e.message}`);
-    }
-
+    const bundlerConfigs = getEsbuildOptions(workingDir, pluginOverrides, bundlerOverrides);
+    const { errors } = await buildWithEsbuild(bundlerConfigs);
     return { cleanup: getCleanupFunction('ESBuild', [bundlerConfigs.outdir]), errors };
 };
 
 export const runVite: BundlerRunFunction = async (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Options = {},
     bundlerOverrides: Partial<RollupOptions> = {},
 ) => {
-    const bundlerConfigs = getViteOptions(seed, pluginOverrides, bundlerOverrides);
-    const vite = await import('vite');
-    const errors = [];
-    try {
-        await vite.build(bundlerConfigs);
-    } catch (e: any) {
-        console.error(`Build failed for Vite`, e);
-        errors.push(`[VITE] : ${e.message}`);
-    }
+    const bundlerConfigs = getViteOptions(workingDir, pluginOverrides, bundlerOverrides);
+    const { errors } = await buildWithVite(bundlerConfigs);
 
     const outdirs: (string | undefined)[] = [];
     if (Array.isArray(bundlerConfigs.build?.rollupOptions?.output)) {
@@ -195,33 +120,12 @@ export const runVite: BundlerRunFunction = async (
 };
 
 export const runRollup: BundlerRunFunction = async (
-    seed: string,
+    workingDir: string,
     pluginOverrides: Options = {},
     bundlerOverrides: Partial<RollupOptions> = {},
 ) => {
-    const bundlerConfigs = getRollupOptions(seed, pluginOverrides, bundlerOverrides);
-    const { rollup } = await import('rollup');
-    const errors = [];
-
-    try {
-        const result = await rollup(bundlerConfigs);
-
-        // Write out the results.
-        if (bundlerConfigs.output) {
-            const outputProms = [];
-            const outputOptions = Array.isArray(bundlerConfigs.output)
-                ? bundlerConfigs.output
-                : [bundlerConfigs.output];
-            for (const outputOption of outputOptions) {
-                outputProms.push(result.write(outputOption));
-            }
-
-            await Promise.all(outputProms);
-        }
-    } catch (e: any) {
-        console.error(`Build failed for Rollup`, e);
-        errors.push(`[ROLLUP] : ${e.message}`);
-    }
+    const bundlerConfigs = getRollupOptions(workingDir, pluginOverrides, bundlerOverrides);
+    const { errors } = await buildWithRollup(bundlerConfigs);
 
     const outdirs: (string | undefined)[] = [];
     if (Array.isArray(bundlerConfigs.output)) {
@@ -272,47 +176,47 @@ const allBundlers: Bundler[] = [
     },
 ];
 
+const requestedBundlers = REQUESTED_BUNDLERS ? REQUESTED_BUNDLERS.split(',') : [];
 export const BUNDLERS: Bundler[] = allBundlers.filter(
-    (bundler) => REQUESTED_BUNDLERS.length === 0 || REQUESTED_BUNDLERS.includes(bundler.name),
+    (bundler) => requestedBundlers.length === 0 || requestedBundlers.includes(bundler.name),
 );
 
 // Build only if needed.
 if (NEED_BUILD) {
-    const bundlersToBuild = new Set(
-        BUNDLERS.map((bundler) => `@datadog/${bundler.name.replace(/\d/g, '')}-plugin`),
-    );
-
-    for (const bundler of bundlersToBuild) {
-        console.log(`Building ${green(bundler)}...`);
-        executeSync('yarn', ['workspace', bundler, 'run', 'build']);
-    }
+    const bundlersToBuild = BUNDLERS.map(({ name }) => name);
+    console.log(`[BUILD] Building ${green(bundlersToBuild.join(', '))}...`);
+    buildPlugins(bundlersToBuild);
+    console.log(`[BUILD] Done.`);
 }
 
 export const runBundlers = async (
     pluginOverrides: Partial<Options> = {},
-    bundlerOverrides: Record<string, any> = {},
+    bundlerOverrides?: BundlerOverrides,
     bundlers?: string[],
-): Promise<CleanupFn> => {
-    const cleanups: CleanupFn[] = [];
+): Promise<CleanupEverythingFn> => {
     const errors: string[] = [];
 
     // Generate a seed to avoid collision of builds.
-    const seed: string = `${Date.now()}-${jest.getSeed()}`;
+    const seed: string = `${jest.getSeed()}.${getUniqueId()}`;
 
     const bundlersToRun = BUNDLERS.filter(
         (bundler) => !bundlers || bundlers.includes(bundler.name),
     );
 
+    const workingDir = await prepareWorkingDir(seed);
+
+    const bundlerOverridesResolved =
+        typeof bundlerOverrides === 'function'
+            ? bundlerOverrides(workingDir)
+            : bundlerOverrides || {};
+
     const runBundlerFunction = async (bundler: Bundler) => {
-        let bundlerOverride = {};
-        if (bundlerOverrides[bundler.name]) {
-            bundlerOverride = bundlerOverrides[bundler.name];
-        }
+        const bundlerOverride = bundlerOverridesResolved[bundler.name] || {};
 
         let result: Awaited<ReturnType<BundlerRunFunction>>;
         // Isolate each runs to avoid conflicts between tests.
         await jest.isolateModulesAsync(async () => {
-            result = await bundler.run(seed, pluginOverrides, bundlerOverride);
+            result = await bundler.run(workingDir, pluginOverrides, bundlerOverride);
         });
         return result!;
     };
@@ -323,25 +227,19 @@ export const runBundlers = async (
         // eslint-disable-next-line no-await-in-loop
         results.push(await runBundlerFunction(bundler));
     }
-    cleanups.push(...results.map((result) => result.cleanup));
     errors.push(...results.map((result) => result.errors).flat());
 
-    // Add a cleanup for the root seeded directory.
-    cleanups.push(getCleanupFunction('Root', [path.resolve(defaultDestination, seed)]));
-
     const cleanupEverything = async () => {
         try {
-            await Promise.all(cleanups.map((cleanup) => cleanup()));
+            // Cleanup working directory.
+            await getCleanupFunction('Root', [workingDir])();
         } catch (e) {
             console.error('Error during cleanup', e);
         }
     };
 
-    if (errors.length) {
-        // We'll throw, so clean everything first.
-        await cleanupEverything();
-        throw new Error(errors.join('\n'));
-    }
+    cleanupEverything.errors = errors;
+    cleanupEverything.workingDir = workingDir;
 
     // Return a cleanUp function.
     return cleanupEverything;
diff --git a/packages/tests/src/_jest/helpers/tips.ts b/packages/tests/src/_jest/helpers/tips.ts
deleted file mode 100644
index 7af7394fb..000000000
--- a/packages/tests/src/_jest/helpers/tips.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants';
-import type { BundlerFullName } from '@dd/core/types';
-import { bgYellow, dim, green, red } from '@dd/tools/helpers';
-
-import { NEED_BUILD, NO_CLEANUP, REQUESTED_BUNDLERS } from './constants';
-
-export const logTips = () => {
-    if (NO_CLEANUP) {
-        console.log(bgYellow(" Won't clean up "));
-    }
-
-    if (NEED_BUILD) {
-        console.log(bgYellow(' Will also build used plugins '));
-    }
-
-    if (REQUESTED_BUNDLERS.length) {
-        if (
-            !(REQUESTED_BUNDLERS as BundlerFullName[]).every((bundler) =>
-                FULL_NAME_BUNDLERS.includes(bundler),
-            )
-        ) {
-            throw new Error(
-                `Invalid "${red(`--bundlers ${REQUESTED_BUNDLERS.join(',')}`)}".\nValid bundlers are ${FULL_NAME_BUNDLERS.map(
-                    (b) => green(b),
-                )
-                    .sort()
-                    .join(', ')}.`,
-            );
-        }
-        const bundlersList = REQUESTED_BUNDLERS.map((bundler) => green(bundler)).join(', ');
-        console.log(`Running ${bgYellow(' ONLY ')} for ${bundlersList}.`);
-    }
-
-    if (!NO_CLEANUP || !NEED_BUILD || REQUESTED_BUNDLERS.length) {
-        const tips: string[] = [];
-        if (!NO_CLEANUP) {
-            tips.push(`  ${green('--cleanup=0')} to keep the built artifacts.`);
-        }
-        if (!NEED_BUILD) {
-            tips.push(`  ${green('--build=1')} to force the build of the used plugins.`);
-        }
-        if (!REQUESTED_BUNDLERS.length) {
-            tips.push(`  ${green('--bundlers=webpack4,esbuild')} to only use specified bundlers.`);
-        }
-        console.log(dim(`\nYou can also use : \n${tips.join('\n')}\n`));
-    }
-};
diff --git a/packages/tests/src/_jest/helpers/types.ts b/packages/tests/src/_jest/helpers/types.ts
index 62935f3a6..30c0ce84a 100644
--- a/packages/tests/src/_jest/helpers/types.ts
+++ b/packages/tests/src/_jest/helpers/types.ts
@@ -9,7 +9,7 @@ import type { RollupOptions } from 'rollup';
 import type { Configuration as Configuration4 } from 'webpack4';
 import type { Configuration } from 'webpack5';
 
-export type BundlerOverrides = {
+export type BundlerOptionsOverrides = {
     rollup?: Partial<RollupOptions>;
     vite?: Partial<RollupOptions>;
     esbuild?: Partial<BuildOptions>;
@@ -18,6 +18,10 @@ export type BundlerOverrides = {
     webpack4?: Partial<Configuration4>;
 };
 
+export type BundlerOverrides =
+    | BundlerOptionsOverrides
+    | ((workingDir: string) => BundlerOptionsOverrides);
+
 export type Bundler = {
     name: BundlerFullName;
     // TODO: Better type this without "any".
@@ -27,6 +31,10 @@ export type Bundler = {
 };
 
 export type CleanupFn = () => Promise<void>;
+export type CleanupEverythingFn = CleanupFn & {
+    errors: string[];
+    workingDir: string;
+};
 export type BundlerRunFunction = (
     seed: string,
     pluginOverrides: Options,
diff --git a/packages/tests/src/_jest/helpers/xpackConfigs.ts b/packages/tests/src/_jest/helpers/xpackConfigs.ts
deleted file mode 100644
index d6e16efee..000000000
--- a/packages/tests/src/_jest/helpers/xpackConfigs.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-// 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 { getResolvedPath } from '@dd/core/helpers';
-import type { Options } from '@dd/core/types';
-import { buildPluginFactory } from '@dd/factory';
-import type { RspackOptions } from '@rspack/core';
-import path from 'path';
-import type webpack4 from 'webpack4';
-import type { Configuration as Configuration4 } from 'webpack4';
-import type { Configuration as Configuration5 } from 'webpack5';
-import type webpack5 from 'webpack5';
-
-import { PLUGIN_VERSIONS } from './constants';
-import { defaultDestination, defaultEntry } from './mocks';
-
-export const getBaseXpackConfig = (
-    seed: string,
-    bundlerName: string,
-): Configuration5 & Configuration4 & RspackOptions => {
-    return {
-        entry: defaultEntry,
-        mode: 'production',
-        output: {
-            path: path.join(defaultDestination, seed, bundlerName),
-            filename: `[name].js`,
-        },
-        devtool: 'source-map',
-        optimization: {
-            minimize: false,
-            splitChunks: {
-                chunks: 'initial',
-                minSize: 1,
-                minChunks: 1,
-                name: (...args: any[]) => {
-                    // This is supposedly not available on rspack (based on types).
-                    // But it is.
-                    if (args[2]) {
-                        return `chunk.${args[2]}`;
-                    }
-
-                    // This is never reached.
-                    return `chunk.shouldNeverHappen`;
-                },
-            },
-        },
-    };
-};
-
-// Return the correct plugin for webpack 4 or 5.
-export const getWebpackPlugin = (
-    pluginOptions: Options,
-    bundler: typeof webpack4 | typeof webpack5,
-) => {
-    // Need to use the factory directly since we pass the bundler to the factory.
-    return buildPluginFactory({
-        bundler,
-        version: PLUGIN_VERSIONS.webpack,
-    }).webpack(pluginOptions);
-};
-
-// Webpack 4 doesn't support pnp resolution OOTB.
-export const getWebpack4Entries = (
-    entries: NonNullable<Configuration5['entry']>,
-    cwd: string = process.cwd(),
-): Configuration4['entry'] => {
-    const getTrueRelativePath = (filepath: string) => {
-        return `./${path.relative(cwd, getResolvedPath(filepath))}`;
-    };
-
-    if (typeof entries === 'string') {
-        return getTrueRelativePath(entries);
-    }
-
-    return Object.fromEntries(
-        Object.entries(entries).map(([name, filepath]) => [
-            name,
-            Array.isArray(filepath)
-                ? filepath.map(getTrueRelativePath)
-                : getTrueRelativePath(filepath),
-        ]),
-    );
-};
diff --git a/packages/tests/src/_jest/setupAfterEnv.ts b/packages/tests/src/_jest/setupAfterEnv.ts
index 32e1b7dd7..c48729407 100644
--- a/packages/tests/src/_jest/setupAfterEnv.ts
+++ b/packages/tests/src/_jest/setupAfterEnv.ts
@@ -6,26 +6,22 @@ import console from 'console';
 import nock from 'nock';
 
 import { toBeWithinRange } from './toBeWithinRange.ts';
-import { toRepeatStringRange } from './toRepeatStringRange.ts';
 import { toRepeatStringTimes } from './toRepeatStringTimes.ts';
 
 // Extend Jest's expect with custom matchers.
 expect.extend({
     toBeWithinRange,
     toRepeatStringTimes,
-    toRepeatStringRange,
 });
 
 interface CustomMatchers<R> {
     toBeWithinRange(floor: number, ceiling: number): R;
-    toRepeatStringTimes(st: string, occurences: number): R;
-    toRepeatStringRange(st: string, range: [number, number]): R;
+    toRepeatStringTimes(st: string | RegExp, occurences: number | [number, number]): R;
 }
 
 interface NonCustomMatchers {
     toBeWithinRange(floor: number, ceiling: number): number;
-    toRepeatStringTimes(st: string, occurences: number): string;
-    toRepeatStringRange(st: string, range: [number, number]): string;
+    toRepeatStringTimes(st: string | RegExp, occurences: number | [number, number]): string;
 }
 
 declare global {
@@ -41,4 +37,5 @@ declare global {
 nock.disableNetConnect();
 
 // Have a simpler, less verbose, console.log output.
+// This bypasses Jest's --silent flag though.
 global.console = console;
diff --git a/packages/tests/src/_jest/toRepeatStringRange.ts b/packages/tests/src/_jest/toRepeatStringRange.ts
deleted file mode 100644
index f0b5cf863..000000000
--- a/packages/tests/src/_jest/toRepeatStringRange.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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 { MatcherFunction } from 'expect';
-
-export const toRepeatStringRange: MatcherFunction<[st: string, range: [number, number]]> =
-    // `st` and `occurences` get types from the line above
-    function toRepeatStringRange(actual, st, range) {
-        if (typeof actual !== 'string' || typeof st !== 'string') {
-            throw new TypeError('Only works with strings.');
-        }
-        if (!Array.isArray(range) || range.length !== 2) {
-            throw new TypeError('Need an array of two numbers for "range".');
-        }
-
-        const { truncateString } = jest.requireActual('@dd/core/helpers');
-        const result = actual.split(st).length - 1;
-        const pass = result <= range[1] && result >= range[0];
-
-        const time = (num: number) => (num > 1 ? 'times' : 'time');
-        const failure = !pass
-            ? `\nBut got it ${this.utils.printReceived(result)} ${time(result)}.`
-            : '.';
-        const expected = this.utils.printReceived(truncateString(actual).replace(/\n/g, ' '));
-
-        const message = `Expected: ${expected}
-To repeat ${this.utils.printExpected(st)}
-Between ${this.utils.printExpected(`${range[0]} and ${range[1]}`)} times${failure}`;
-
-        return {
-            message: () => message,
-            pass,
-        };
-    };
diff --git a/packages/tests/src/_jest/toRepeatStringTimes.ts b/packages/tests/src/_jest/toRepeatStringTimes.ts
index 053fb3e65..d54cbbeea 100644
--- a/packages/tests/src/_jest/toRepeatStringTimes.ts
+++ b/packages/tests/src/_jest/toRepeatStringTimes.ts
@@ -4,29 +4,38 @@
 
 import type { MatcherFunction } from 'expect';
 
-export const toRepeatStringTimes: MatcherFunction<[st: string, occurences: number]> =
+export const toRepeatStringTimes: MatcherFunction<
+    [st: string | RegExp, occurences: number | [number, number]]
+> =
     // `st` and `occurences` get types from the line above
     function toRepeatStringTimes(actual, st, occurences) {
-        if (typeof actual !== 'string' || typeof st !== 'string') {
-            throw new TypeError('Only works with strings.');
+        if (typeof actual !== 'string' || (typeof st !== 'string' && !(st instanceof RegExp))) {
+            throw new TypeError('Only works with strings or RegExp.');
         }
-        if (typeof occurences !== 'number') {
-            throw new TypeError('Need a number here.');
+        if (
+            typeof occurences !== 'number' &&
+            (!Array.isArray(occurences) || occurences.length !== 2)
+        ) {
+            throw new TypeError('Need a number or an array of two numbers.');
         }
 
         const { truncateString } = jest.requireActual('@dd/core/helpers');
         const result = actual.split(st).length - 1;
-        const pass = result === occurences;
+        const isRange = Array.isArray(occurences);
+        const pass = isRange
+            ? result <= occurences[1] && result >= occurences[0]
+            : result === occurences;
 
         const time = (num: number) => (num > 1 ? 'times' : 'time');
         const failure = !pass
-            ? `\nBut got it ${this.utils.printReceived(result)} ${time(result)}.`
-            : '';
+            ? `\n\nBut got it ${this.utils.printReceived(result)} ${time(result)}.`
+            : '.';
         const expected = this.utils.printReceived(truncateString(actual).replace(/\n/g, ' '));
+        const expectedSt = isRange
+            ? `Between ${this.utils.printExpected(`${occurences[0]} and ${occurences[1]}`)} times${failure}`
+            : `Exactly ${this.utils.printExpected(occurences)} ${time(occurences)}${failure}`;
 
-        const message = `Expected: ${expected}
-To repeat ${this.utils.printExpected(st)}
-Exactly ${this.utils.printExpected(occurences)} ${time(occurences)}${failure}.`;
+        const message = `Expected: ${expected}\nTo repeat ${this.utils.printExpected(st)}\n${expectedSt}`;
 
         return {
             message: () => message,
diff --git a/packages/tests/src/_playwright/constants.ts b/packages/tests/src/_playwright/constants.ts
new file mode 100644
index 000000000..d8018dbf0
--- /dev/null
+++ b/packages/tests/src/_playwright/constants.ts
@@ -0,0 +1,10 @@
+// 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 { ROOT } from '@dd/tools/constants';
+import path from 'path';
+
+export const PUBLIC_DIR = path.resolve(ROOT, 'packages/tests/src/_playwright/public');
+export const DEV_SERVER_PORT = 8000;
+export const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`;
diff --git a/packages/tests/src/_playwright/globalSetup.ts b/packages/tests/src/_playwright/globalSetup.ts
new file mode 100644
index 000000000..ed4bdb914
--- /dev/null
+++ b/packages/tests/src/_playwright/globalSetup.ts
@@ -0,0 +1,47 @@
+// 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 { ENV_VAR_REQUESTED_BUNDLERS } from '@dd/core/constants';
+import { rm } from '@dd/core/helpers';
+import type { BundlerFullName } from '@dd/core/types';
+import { getRequestedBundlers } from '@dd/tests/_playwright/helpers/requestedBundlers';
+import type { TestOptions } from '@dd/tests/_playwright/testParams';
+import { blue, buildPlugins, dim, green } from '@dd/tools/helpers';
+import type { FullConfig } from '@playwright/test';
+import { glob } from 'glob';
+import path from 'path';
+
+// TODO Also build and test for ESM.
+const globalSetup = async (config: FullConfig<TestOptions>) => {
+    const getPfx = (name: string) => `[${blue(name)}] `;
+    const getSubPfx = (name: string) => `  ${dim(getPfx(name))}`;
+    const globalPfx = getPfx('Global Setup');
+    console.time(globalPfx);
+    const requestedBundlers = getRequestedBundlers();
+    // Save the requested bundlers in the env.
+    process.env[ENV_VAR_REQUESTED_BUNDLERS] = requestedBundlers.join(',');
+    console.log(`${globalPfx}Setting up tests.`);
+
+    // In the CI we're building before the job starts.
+    // No need to do it here.
+    if (!process.env.CI) {
+        // Build the requested bundler plugins.
+        const buildPluginsPfx = getSubPfx('Build Plugins');
+        console.time(buildPluginsPfx);
+        console.log(`${buildPluginsPfx}Building ${green(requestedBundlers.join(', '))} plugins...`);
+        buildPlugins(requestedBundlers as BundlerFullName[]);
+        console.timeEnd(buildPluginsPfx);
+    }
+
+    // Delete public dirs.
+    const cleanPfx = getSubPfx('Clean');
+    console.time(cleanPfx);
+    const publicDirs = await glob('public/*/', { cwd: __dirname });
+    await Promise.all(publicDirs.map((dir) => rm(path.resolve(__dirname, dir))));
+    console.timeEnd(cleanPfx);
+
+    console.timeEnd(globalPfx);
+};
+
+export default globalSetup;
diff --git a/packages/tests/src/_playwright/helpers/buildProject.ts b/packages/tests/src/_playwright/helpers/buildProject.ts
new file mode 100644
index 000000000..32a2a5845
--- /dev/null
+++ b/packages/tests/src/_playwright/helpers/buildProject.ts
@@ -0,0 +1,128 @@
+// 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 { mkdir, rm } from '@dd/core/helpers';
+import type { BundlerFullName } from '@dd/core/types';
+import { allBundlers } from '@dd/tools/bundlers';
+import { dim } from '@dd/tools/helpers';
+import { allPlugins, fullConfig } from '@dd/tools/plugins';
+import fs from 'fs';
+import path from 'path';
+
+// Build a given project with a given bundler.
+const buildProject = async (bundler: BundlerFullName, cwd: string) => {
+    const plugin = allPlugins[bundler](fullConfig);
+    const build = allBundlers[bundler];
+    const buildConfig = build.config({
+        workingDir: cwd,
+        // We'll use the name of the bundler as the name of the entry file.
+        // eg: For "Webpack 4" we'll have "./projects/dist/webpack4.js".
+        entry: { [bundler]: './index.js' },
+        outDir: path.resolve(cwd, './dist'),
+        plugins: [plugin],
+    });
+
+    return build.run(buildConfig);
+};
+
+// Build a given project with a list of bundlers.
+const buildProjectWithBundlers = async (projectPath: string, bundlers: BundlerFullName[]) => {
+    const name = projectPath.split(path.sep).pop() || 'unknown';
+
+    // Clean the dist folders.
+    await rm(path.resolve(projectPath, 'dist'));
+
+    // Build with all the bundlers.
+    return Promise.all(
+        bundlers.map(async (bundler) => {
+            const buildBundlerPfx = `    [${dim(`Build ${name} with ${bundler}`)}]`;
+            console.time(buildBundlerPfx);
+            const { errors } = await buildProject(bundler, projectPath);
+            console.timeEnd(buildBundlerPfx);
+            return errors;
+        }),
+    );
+};
+
+// Wrapper around the buildProjectWithBundlers function.
+//   - Create the destination folder.
+//   - Copy the content of the source folder in it.
+//   - Build the project with all the requested bundlers.
+//   - Delete the folder if the build failed.
+//   - Touch a "built" file if the build succeeded.
+const handleBuild = async (source: string, destination: string, bundlers: BundlerFullName[]) => {
+    // Create the project dir.
+    await mkdir(destination);
+    // Copy the content of our project in it.
+    await fs.promises.cp(`${source}/`, `${destination}/`, {
+        recursive: true,
+        errorOnExist: true,
+        force: true,
+    });
+
+    // Build it with all the requested bundlers.
+    const name = destination.split(path.sep).pop() || 'unknown';
+    const buildProjectPfx = `  [${dim(name)}] `;
+    console.time(buildProjectPfx);
+    const errors = (await buildProjectWithBundlers(destination, bundlers)).flat();
+
+    if (errors.length) {
+        console.error(`${buildProjectPfx}Build failed.`, errors);
+        // Delete the folder, so other tests can try and build it.
+        await rm(destination);
+    } else {
+        // Touch the built file so other tests know it's ready.
+        await fs.promises.writeFile(`${destination}/built`, '');
+    }
+
+    console.timeEnd(buildProjectPfx);
+};
+
+// Wait for the build to be done or to fail.
+// Based on the presence of the "built" file or the disparition of the project folder.
+const waitForBuild = async (projectDir: string): Promise<{ built: boolean; error: boolean }> => {
+    return new Promise((resolve) => {
+        const builtInterval = setInterval(() => {
+            if (fs.existsSync(`${projectDir}/built`)) {
+                clearInterval(builtInterval);
+                clearInterval(errorInterval);
+                resolve({ built: true, error: false });
+            }
+        }, 100);
+
+        const errorInterval = setInterval(() => {
+            if (!fs.existsSync(projectDir)) {
+                clearInterval(errorInterval);
+                clearInterval(builtInterval);
+                resolve({ built: false, error: true });
+            }
+        }, 100);
+    });
+};
+
+// Verify if the project has been built.
+// Trigger the build if it's not been done yet.
+// Wait for the build to be done.
+// Note: This is to be used in a beforeAll hook,
+// so all the workers can use the same build of a given suite.
+export const verifyProjectBuild = async (
+    source: string,
+    destination: string,
+    bundlers: BundlerFullName[],
+) => {
+    // Wait a random time to avoid conflicts.
+    await new Promise<void>((resolve) => setTimeout(resolve, Math.floor(Math.random() * 500)));
+
+    // Verify if the build as started, by checking the presence of the public directory.
+    const dirExists = fs.existsSync(destination);
+    if (dirExists) {
+        const result = await waitForBuild(destination);
+        if (result.error) {
+            await verifyProjectBuild(source, destination, bundlers);
+        }
+    } else {
+        // Build the project.
+        await handleBuild(source, destination, bundlers);
+    }
+};
diff --git a/packages/tests/src/_playwright/helpers/requestedBundlers.ts b/packages/tests/src/_playwright/helpers/requestedBundlers.ts
new file mode 100644
index 000000000..07fcbde58
--- /dev/null
+++ b/packages/tests/src/_playwright/helpers/requestedBundlers.ts
@@ -0,0 +1,55 @@
+// 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 { ENV_VAR_REQUESTED_BUNDLERS, FULL_NAME_BUNDLERS } from '@dd/core/constants';
+import type { BundlerFullName } from '@dd/core/types';
+import { yellow } from '@dd/tools/helpers';
+
+const getBundlerNameFromProject = (projectName: string) => {
+    return projectName.split(' | ')[1] as BundlerFullName;
+};
+
+// Parse and detect all the --project arguments to get the requested bundlers.
+export const getRequestedBundlers = (
+    baseBundlers: readonly BundlerFullName[] = FULL_NAME_BUNDLERS,
+): BundlerFullName[] => {
+    if (process.env[ENV_VAR_REQUESTED_BUNDLERS]) {
+        return process.env[ENV_VAR_REQUESTED_BUNDLERS].split(',') as BundlerFullName[];
+    }
+
+    const requestedBundlers: Set<BundlerFullName> = new Set();
+    let capture = false;
+
+    for (const arg of process.argv) {
+        let bundlerName: BundlerFullName;
+        if (arg === '--project') {
+            // Capture the next argument as the bundler name.
+            capture = true;
+            continue;
+        }
+
+        if (capture === true) {
+            // Capture pass.
+            bundlerName = getBundlerNameFromProject(arg.trim());
+            capture = false;
+        } else if (arg.startsWith('--project=')) {
+            // Argument is already in the format --project=...
+            bundlerName = getBundlerNameFromProject(arg.split('=')[1].trim());
+        } else {
+            continue;
+        }
+
+        if (baseBundlers.includes(bundlerName) && FULL_NAME_BUNDLERS.includes(bundlerName)) {
+            requestedBundlers.add(bundlerName);
+        } else {
+            console.warn(yellow(`Bundler "${bundlerName}" is not available.`));
+        }
+    }
+
+    const requestBundlers: BundlerFullName[] = requestedBundlers.size
+        ? Array.from(requestedBundlers)
+        : [...baseBundlers];
+
+    return requestBundlers;
+};
diff --git a/packages/tests/src/_playwright/public/.gitignore b/packages/tests/src/_playwright/public/.gitignore
new file mode 100644
index 000000000..a2632cea4
--- /dev/null
+++ b/packages/tests/src/_playwright/public/.gitignore
@@ -0,0 +1,6 @@
+# We don't want anything else in this folder.
+# It is used for e2e tests to serve their own apps.
+*
+!.gitignore
+!404.html
+!index.html
diff --git a/packages/tests/src/_playwright/public/404.html b/packages/tests/src/_playwright/public/404.html
new file mode 100644
index 000000000..f8d67d86c
--- /dev/null
+++ b/packages/tests/src/_playwright/public/404.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="icon" type="image/svg+xml" sizes="21x21" href="data:image/svg+xml,">
+    <title>404 Not Found</title>
+</head>
+
+<body>
+    <h1>404 - Page Not Found for {{bundler}}</h1>
+    <p>Sorry, the page you are looking for does not exist.</p>
+    <a href="./index.html">Go back to Home Page</a>
+</body>
+
+</html>
diff --git a/packages/tests/src/_playwright/public/index.html b/packages/tests/src/_playwright/public/index.html
new file mode 100644
index 000000000..c4a889feb
--- /dev/null
+++ b/packages/tests/src/_playwright/public/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="icon" type="image/svg+xml" sizes="21x21" href="data:image/svg+xml,">
+    <title>Welcome</title>
+</head>
+
+<body>
+    <h1>Welcome</h1>
+</body>
+
+</html>
diff --git a/packages/tests/src/_playwright/testParams.ts b/packages/tests/src/_playwright/testParams.ts
new file mode 100644
index 000000000..66d1dcaf7
--- /dev/null
+++ b/packages/tests/src/_playwright/testParams.ts
@@ -0,0 +1,42 @@
+// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants';
+import type { BundlerFullName } from '@dd/core/types';
+import { DEV_SERVER_URL, PUBLIC_DIR } from '@dd/tests/_playwright/constants';
+import { test as base } from '@playwright/test';
+import path from 'path';
+
+export type TestOptions = {
+    bundler: BundlerFullName;
+    bundlers: BundlerFullName[];
+};
+
+type Fixtures = {
+    devServerUrl: string;
+    publicDir: string;
+    suiteName: string;
+};
+
+export const test = base.extend<TestOptions & Fixtures>({
+    // Default value, will be overridden by config.
+    bundler: ['rollup', { option: true }],
+    bundlers: [[...FULL_NAME_BUNDLERS], { option: true }],
+    devServerUrl: [
+        // eslint-disable-next-line no-empty-pattern
+        async ({}, use, info) => {
+            const url = info.config.webServer?.url || DEV_SERVER_URL;
+            await use(url);
+        },
+        { auto: true },
+    ],
+    suiteName: [
+        // eslint-disable-next-line no-empty-pattern
+        async ({}, use, info) => {
+            await use(path.dirname(info.file).split(path.sep).pop() || 'unknown');
+        },
+        { auto: true },
+    ],
+    publicDir: PUBLIC_DIR,
+});
diff --git a/packages/tests/src/core/helpers.test.ts b/packages/tests/src/core/helpers.test.ts
deleted file mode 100644
index 94f6d4313..000000000
--- a/packages/tests/src/core/helpers.test.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-// 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 { RequestOpts } from '@dd/core/types';
-import { API_PATH, FAKE_URL, INTAKE_URL } from '@dd/tests/_jest/helpers/mocks';
-import nock from 'nock';
-import { Readable } from 'stream';
-import { createGzip } from 'zlib';
-
-// Reduce the retry timeout to speed up the tests.
-jest.mock('async-retry', () => {
-    const original = jest.requireActual('async-retry');
-    return jest.fn((callback, options) => {
-        return original(callback, {
-            ...options,
-            minTimeout: 0,
-            maxTimeout: 1,
-        });
-    });
-});
-
-describe('Core Helpers', () => {
-    describe('formatDuration', () => {
-        test.each([
-            [10, '10ms'],
-            [10010, '10s 10ms'],
-            [1000010, '16m 40s 10ms'],
-            [10000010, '2h 46m 40s 10ms'],
-            [1000000010, '11d 13h 46m 40s 10ms'],
-        ])('Should format duration %s => %s', async (ms, expected) => {
-            const { formatDuration } = await import('@dd/core/helpers');
-            expect(formatDuration(ms)).toBe(expected);
-        });
-    });
-
-    describe('doRequest', () => {
-        const getDataStream = () => {
-            const gz = createGzip();
-            const stream = new Readable();
-            stream.push('Some data');
-            stream.push(null);
-            return stream.pipe(gz);
-        };
-
-        const getDataMock = () => ({
-            data: getDataStream(),
-            headers: {
-                'Content-Encoding': 'gzip',
-            },
-        });
-
-        const requestOpts: RequestOpts = {
-            url: INTAKE_URL,
-            method: 'POST',
-            type: 'json',
-            getData: getDataMock,
-        };
-
-        afterEach(() => {
-            nock.cleanAll();
-        });
-
-        test('Should do a request', async () => {
-            const { doRequest } = await import('@dd/core/helpers');
-            const scope = nock(FAKE_URL).post(API_PATH).reply(200, {});
-
-            const response = await doRequest(requestOpts);
-
-            expect(scope.isDone()).toBe(true);
-            expect(response).toEqual({});
-        });
-
-        test('Should retry on error', async () => {
-            const { doRequest } = await import('@dd/core/helpers');
-            // Success after 2 retries.
-            const scope = nock(FAKE_URL)
-                .post(API_PATH)
-                .times(2)
-                .reply(404)
-                .post(API_PATH)
-                .reply(200, { data: 'ok' });
-
-            const response = await doRequest(requestOpts);
-
-            expect(scope.isDone()).toBe(true);
-            expect(response).toEqual({ data: 'ok' });
-        });
-
-        test('Should throw on too many retries', async () => {
-            const { doRequest } = await import('@dd/core/helpers');
-            const scope = nock(FAKE_URL)
-                .post(API_PATH)
-                .times(6)
-                .reply(500, 'Internal Server Error');
-
-            await expect(async () => {
-                await doRequest(requestOpts);
-            }).rejects.toThrow('HTTP 500 Internal Server Error');
-            expect(scope.isDone()).toBe(true);
-        });
-
-        test('Should bail on specific status', async () => {
-            const { doRequest } = await import('@dd/core/helpers');
-            const scope = nock(FAKE_URL).post(API_PATH).reply(400, 'Bad Request');
-
-            await expect(async () => {
-                await doRequest(requestOpts);
-            }).rejects.toThrow('HTTP 400 Bad Request');
-            expect(scope.isDone()).toBe(true);
-        });
-
-        test('Should bail on unrelated errors', async () => {
-            const { doRequest } = await import('@dd/core/helpers');
-            const scope = nock(FAKE_URL).post(API_PATH).reply(404);
-            // Creating the data stream outside should make the fetch invocation fail
-            // on the second pass as it will try to read an already consumed stream.
-            const data = getDataStream();
-
-            await expect(async () => {
-                await doRequest({ ...requestOpts, getData: () => ({ data, headers: {} }) });
-            }).rejects.toThrow('Response body object should not be disturbed or locked');
-            expect(scope.isDone()).toBe(true);
-        });
-    });
-
-    describe('truncateString', () => {
-        test.each([
-            // No truncation needed.
-            ['Short string', 20, '[...]', 'Short string'],
-            // Keep at least 2 characters on each side.
-            ['Short string', 2, '[...]', 'Sh[...]ng'],
-            // Equaly truncate on both sides.
-            [
-                'A way too long sentence could be truncated a bit.',
-                20,
-                '[...]',
-                'A way t[...]d a bit.',
-            ],
-            // Custom placeholder.
-            [
-                'A way too long sentence could be truncated a bit.',
-                20,
-                '***',
-                'A way to***ed a bit.',
-            ],
-            // Longer sentence.
-            [
-                'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
-                50,
-                '[...]',
-                'Lorem ipsu[...]t ut labore et dolore magna aliqua.',
-            ],
-        ])(
-            'Should truncate string "%s" to max length %d with placeholder "%s" => "%s"',
-            async (str, maxLength, placeholder, expected) => {
-                const { truncateString } = await import('@dd/core/helpers');
-                expect(truncateString(str, maxLength, placeholder)).toBe(expected);
-            },
-        );
-    });
-});
diff --git a/packages/tests/src/e2e/smokeTest/project/index.html b/packages/tests/src/e2e/smokeTest/project/index.html
new file mode 100644
index 000000000..9d63a04cc
--- /dev/null
+++ b/packages/tests/src/e2e/smokeTest/project/index.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <link rel="icon" type="image/svg+xml" sizes="21x21" href="data:image/svg+xml,">
+    <title>Basic HTML Page</title>
+</head>
+
+<body>
+    <h1>Welcome to the {{bundler}} Home page</h1>
+    <p>This is just some simple text.</p>
+
+    <script src="./dist/{{bundler}}.js"></script>
+</body>
+
+</html>
diff --git a/packages/tests/src/_jest/fixtures/file-to-inject.js b/packages/tests/src/e2e/smokeTest/project/index.js
similarity index 82%
rename from packages/tests/src/_jest/fixtures/file-to-inject.js
rename to packages/tests/src/e2e/smokeTest/project/index.js
index 8c9d16b3e..d7a4a42ff 100644
--- a/packages/tests/src/_jest/fixtures/file-to-inject.js
+++ b/packages/tests/src/e2e/smokeTest/project/index.js
@@ -2,4 +2,4 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-console.log("Hello injection from local file.");
+console.log('Hello, {{bundler}}!');
diff --git a/packages/tests/src/e2e/smokeTest/smokeTest.spec.ts b/packages/tests/src/e2e/smokeTest/smokeTest.spec.ts
new file mode 100644
index 000000000..4b2078f11
--- /dev/null
+++ b/packages/tests/src/e2e/smokeTest/smokeTest.spec.ts
@@ -0,0 +1,65 @@
+// 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 { verifyProjectBuild } from '@dd/tests/_playwright/helpers/buildProject';
+import type { TestOptions } from '@dd/tests/_playwright/testParams';
+import { test } from '@dd/tests/_playwright/testParams';
+import type { Page } from '@playwright/test';
+import path from 'path';
+
+// Have a similar experience to Jest.
+const { expect, beforeAll, describe } = test;
+
+const userFlow = async (url: string, page: Page, bundler: TestOptions['bundler']) => {
+    // Navigate to our page.
+    await page.goto(`${url}/index.html?context_bundler=${bundler}`);
+    await page.waitForSelector('body');
+};
+
+describe('Smoke Test', () => {
+    // Build our fixture project.
+    beforeAll(async ({ publicDir, bundlers, suiteName }) => {
+        const source = path.resolve(__dirname, 'project');
+        const destination = path.resolve(publicDir, suiteName);
+        await verifyProjectBuild(source, destination, bundlers);
+    });
+
+    test('Should load the page without errors', async ({
+        page,
+        bundler,
+        browserName,
+        suiteName,
+        devServerUrl,
+    }) => {
+        const errors: string[] = [];
+        const testBaseUrl = `${devServerUrl}/${suiteName}`;
+
+        // Listen for errors on the page.
+        page.on('pageerror', (error) => errors.push(error.message));
+        page.on('response', async (response) => {
+            if (!response.ok()) {
+                const url = response.request().url();
+                const prefix = `[${bundler} ${browserName} ${response.status()}]`;
+                errors.push(`${prefix} ${url}`);
+            }
+        });
+
+        // Verify that we do log the expected things.
+        const logs: string[] = [];
+        page.on('console', async (msg) => {
+            for (const arg of msg.args()) {
+                // eslint-disable-next-line no-await-in-loop
+                logs.push(await arg.jsonValue());
+            }
+        });
+
+        // It should load the correct bundler file too.
+        const bundleRequest = page.waitForResponse(`${testBaseUrl}/dist/${bundler}.js`);
+        await userFlow(testBaseUrl, page, bundler);
+        expect((await bundleRequest).ok()).toBe(true);
+
+        expect(logs).toEqual([`Hello, ${bundler}!`]);
+        expect(errors).toHaveLength(0);
+    });
+});
diff --git a/packages/tests/src/plugins/build-report/esbuild.test.ts b/packages/tests/src/plugins/build-report/esbuild.test.ts
deleted file mode 100644
index 56792b59f..000000000
--- a/packages/tests/src/plugins/build-report/esbuild.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-// 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 { getEntryNames } from '@dd/internal-build-report-plugin/esbuild';
-import { getContextMock } from '@dd/tests/_jest/helpers/mocks';
-import { vol } from 'memfs';
-import path from 'path';
-
-jest.mock('fs', () => require('memfs').fs);
-
-describe('Build report plugin esbuild', () => {
-    describe('getEntrynames', () => {
-        beforeEach(() => {
-            // Emulate some fixtures.
-            vol.fromJSON({
-                'fixtures/main.js': '',
-                'fixtures/in/main2.js': '',
-                'fixtures/in/main3.js': '',
-                'fixtures/main4.js': '',
-            });
-        });
-
-        afterEach(() => {
-            vol.reset();
-        });
-        const expectations: [string, Parameters<typeof getEntryNames>[0], Map<string, string>][] = [
-            [
-                'Array of strings',
-                [path.join(process.cwd(), 'fixtures/main.js')],
-                new Map([['fixtures/main.js', 'fixtures/main.js']]),
-            ],
-            [
-                'Object with entry names',
-                {
-                    app1: path.join(process.cwd(), 'fixtures/main.js'),
-                    app2: path.join(process.cwd(), 'fixtures/main4.js'),
-                },
-                new Map([
-                    ['fixtures/main.js', 'app1'],
-                    ['fixtures/main4.js', 'app2'],
-                ]),
-            ],
-            [
-                'Array of objects with in and out',
-                [
-                    {
-                        in: 'fixtures/main.js',
-                        out: 'outdir/main.js',
-                    },
-                ],
-                new Map([['fixtures/main.js', 'fixtures/main.js']]),
-            ],
-            ['undefined', undefined, new Map()],
-            [
-                'Array of strings with glob',
-                [path.join(process.cwd(), 'fixtures/*.js')],
-                new Map([
-                    ['fixtures/main.js', 'fixtures/main.js'],
-                    ['fixtures/main4.js', 'fixtures/main4.js'],
-                ]),
-            ],
-            [
-                'Object with entry names with glob',
-                {
-                    app1: path.join(process.cwd(), 'fixtures/*.js'),
-                    app2: path.join(process.cwd(), 'fixtures/**/*.js'),
-                },
-                new Map([
-                    ['fixtures/main.js', 'app1'],
-                    ['fixtures/in/main2.js', 'app2'],
-                    ['fixtures/in/main3.js', 'app2'],
-                    ['fixtures/main.js', 'app2'],
-                    // We expect the latest entry to take precendence.
-                    ['fixtures/main4.js', 'app2'],
-                ]),
-            ],
-            [
-                'Array of objects with in and out with globs',
-                [
-                    {
-                        in: 'fixtures/*.js',
-                        out: 'outdir/main.js',
-                    },
-                    {
-                        in: 'fixtures/main4.js',
-                        out: 'outdir/main4.js',
-                    },
-                ],
-                new Map([
-                    ['fixtures/main.js', 'fixtures/main.js'],
-                    ['fixtures/main4.js', 'fixtures/main4.js'],
-                ]),
-            ],
-        ];
-        test.each(expectations)(
-            'Should return the right map of entrynames for "%s".',
-            (name, entryPoints, entryNames) => {
-                const result = getEntryNames(
-                    entryPoints,
-                    getContextMock({
-                        cwd: process.cwd(),
-                        bundler: {
-                            name: 'esbuild',
-                            fullName: 'esbuild',
-                            outDir: path.join(process.cwd(), 'outdir'),
-                            version: '1.0.0',
-                        },
-                    }),
-                );
-                expect(result).toEqual(entryNames);
-            },
-        );
-    });
-});
diff --git a/packages/tests/src/plugins/injection/index.test.ts b/packages/tests/src/plugins/injection/index.test.ts
deleted file mode 100644
index d15e006f5..000000000
--- a/packages/tests/src/plugins/injection/index.test.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
-// This product includes software developed at Datadog (https://www.datadoghq.com/).
-// Copyright 2019-Present Datadog, Inc.
-
-import type { Options } from '@dd/core/types';
-import {
-    debugFilesPlugins,
-    getComplexBuildOverrides,
-    getNodeSafeBuildOverrides,
-} from '@dd/tests/_jest/helpers/mocks';
-import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
-import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
-import { execute } from '@dd/tools/helpers';
-import { readFileSync, writeFileSync } from 'fs';
-import { glob } from 'glob';
-import nock from 'nock';
-import path from 'path';
-
-describe('Injection Plugin', () => {
-    const distantFileLog = 'Hello injection from distant file.';
-    const distantFileContent = `console.log("${distantFileLog}");`;
-    const localFileLog = 'Hello injection from local file.';
-    const localFileContent = `console.log("${localFileLog}");`;
-    const codeLog = 'Hello injection from code.';
-    const codeContent = `console.log("${codeLog}");`;
-    let outdirs: Record<string, string> = {};
-
-    const expectations = [
-        { type: 'some string', content: codeContent, log: codeLog },
-        { type: 'a local file', content: localFileContent, log: localFileLog },
-        { type: 'a distant file', content: distantFileContent, log: distantFileLog },
-    ];
-
-    const customPlugins: Options['customPlugins'] = (opts, context) => {
-        context.inject({
-            type: 'file',
-            value: 'https://example.com/distant-file.js',
-        });
-        context.inject({
-            type: 'file',
-            value: './src/_jest/fixtures/file-to-inject.js',
-        });
-        context.inject({
-            type: 'code',
-            value: codeContent,
-        });
-
-        return [
-            {
-                name: 'get-outdirs',
-                writeBundle() {
-                    // Store the seeded outdir to inspect the produced files.
-                    outdirs[context.bundler.fullName] = context.bundler.outDir;
-
-                    // Add a package.json file to the esm builds.
-                    if (['esbuild'].includes(context.bundler.fullName)) {
-                        writeFileSync(
-                            path.resolve(context.bundler.outDir, 'package.json'),
-                            '{ "type": "module" }',
-                        );
-                    }
-                },
-            },
-            ...debugFilesPlugins(context),
-        ];
-    };
-
-    describe('Basic build', () => {
-        let nockScope: nock.Scope;
-        let cleanup: CleanupFn;
-
-        beforeAll(async () => {
-            nockScope = nock('https://example.com')
-                .get('/distant-file.js')
-                .times(BUNDLERS.length)
-                .reply(200, distantFileContent);
-
-            cleanup = await runBundlers(
-                {
-                    customPlugins,
-                },
-                getNodeSafeBuildOverrides(),
-            );
-        });
-
-        afterAll(async () => {
-            outdirs = {};
-            nock.cleanAll();
-            await cleanup();
-        });
-
-        test('Should have requested the distant file for each bundler.', () => {
-            expect(nockScope.isDone()).toBe(true);
-        });
-
-        describe.each(BUNDLERS)('$name | $version', ({ name }) => {
-            let programOutput: string;
-            beforeAll(async () => {
-                // Test the actual bundled file too.
-                const result = await execute('node', [path.resolve(outdirs[name], 'main.js')]);
-                programOutput = result.stdout;
-            });
-
-            test.each(expectations)('Should inject $type once.', async ({ content, log }) => {
-                const files = glob.sync(path.resolve(outdirs[name], '*.{js,mjs}'));
-                const fullContent = files.map((file) => readFileSync(file, 'utf8')).join('\n');
-
-                // We have a single entry, so the content should be repeated only once.
-                expect(fullContent).toRepeatStringTimes(content, 1);
-                // Verify the program output from the bundled project.
-                expect(programOutput).toRepeatStringTimes(log, 1);
-            });
-        });
-    });
-
-    describe('Complex build', () => {
-        let nockScope: nock.Scope;
-        let cleanup: CleanupFn;
-
-        beforeAll(async () => {
-            nockScope = nock('https://example.com')
-                .get('/distant-file.js')
-                .times(BUNDLERS.length)
-                .reply(200, distantFileContent);
-
-            cleanup = await runBundlers(
-                {
-                    customPlugins,
-                },
-                getNodeSafeBuildOverrides(getComplexBuildOverrides()),
-            );
-        });
-
-        afterAll(async () => {
-            outdirs = {};
-            nock.cleanAll();
-            await cleanup();
-        });
-
-        test('Should have requested the distant file for each bundler.', () => {
-            expect(nockScope.isDone()).toBe(true);
-        });
-
-        describe.each(BUNDLERS)('$name | $version', ({ name }) => {
-            let programOutput1: string;
-            let programOutput2: string;
-            beforeAll(async () => {
-                // Test the actual bundled file too.
-                const result1 = await execute('node', [path.resolve(outdirs[name], 'app1.js')]);
-                programOutput1 = result1.stdout;
-                const result2 = await execute('node', [path.resolve(outdirs[name], 'app2.js')]);
-                programOutput2 = result2.stdout;
-            });
-
-            test.each(expectations)('Should inject $type.', ({ content, log }) => {
-                const files = glob.sync(path.resolve(outdirs[name], '*.{js,mjs}'));
-                const fullContent = files.map((file) => readFileSync(file, 'utf8')).join('\n');
-
-                // We don't know exactly how each bundler will concattenate the files.
-                // Since we have two entries here, we can expect the content
-                // to be repeated at least once and at most twice.
-                expect(fullContent).toRepeatStringRange(content, [1, 2]);
-                // Verify the program output from the bundled project.
-                expect(programOutput1).toRepeatStringTimes(log, 1);
-                expect(programOutput2).toRepeatStringTimes(log, 1);
-            });
-        });
-    });
-});
diff --git a/packages/tests/src/unit/core/helpers.test.ts b/packages/tests/src/unit/core/helpers.test.ts
new file mode 100644
index 000000000..d76a1a197
--- /dev/null
+++ b/packages/tests/src/unit/core/helpers.test.ts
@@ -0,0 +1,362 @@
+// 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 { getEsbuildEntries } from '@dd/core/helpers';
+import type { RequestOpts, ResolvedEntry } from '@dd/core/types';
+import {
+    API_PATH,
+    FAKE_URL,
+    INTAKE_URL,
+    getContextMock,
+    getEsbuildMock,
+    mockLogger,
+} from '@dd/tests/_jest/helpers/mocks';
+import type { BuildOptions } from 'esbuild';
+import { vol } from 'memfs';
+import nock from 'nock';
+import path from 'path';
+import { Readable } from 'stream';
+import { createGzip } from 'zlib';
+
+// Reduce the retry timeout to speed up the tests.
+jest.mock('async-retry', () => {
+    const original = jest.requireActual('async-retry');
+    return jest.fn((callback, options) => {
+        return original(callback, {
+            ...options,
+            minTimeout: 0,
+            maxTimeout: 1,
+        });
+    });
+});
+
+// Use mock files.
+jest.mock('fs', () => require('memfs').fs);
+
+describe('Core Helpers', () => {
+    describe('formatDuration', () => {
+        test.each([
+            [10, '10ms'],
+            [10010, '10s 10ms'],
+            [1000010, '16m 40s 10ms'],
+            [10000010, '2h 46m 40s 10ms'],
+            [1000000010, '11d 13h 46m 40s 10ms'],
+        ])('Should format duration %s => %s', async (ms, expected) => {
+            const { formatDuration } = await import('@dd/core/helpers');
+            expect(formatDuration(ms)).toBe(expected);
+        });
+    });
+
+    describe('getEsbuildEntries', () => {
+        beforeEach(() => {
+            // Emulate some fixtures.
+            vol.fromJSON({
+                'fixtures/main.js': '',
+                'fixtures/in/main2.js': '',
+                'fixtures/in/main3.js': '',
+                'fixtures/main4.js': '',
+            });
+        });
+
+        afterEach(() => {
+            vol.reset();
+        });
+
+        const expectations: [string, BuildOptions['entryPoints'], ResolvedEntry[]][] = [
+            [
+                'Array of strings',
+                [path.join(process.cwd(), 'fixtures/main.js')],
+                [
+                    {
+                        original: path.join(process.cwd(), 'fixtures/main.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                ],
+            ],
+            [
+                'Object with entry names',
+                {
+                    app1: path.join(process.cwd(), 'fixtures/main.js'),
+                    app2: path.join(process.cwd(), 'fixtures/main4.js'),
+                },
+                [
+                    {
+                        name: 'app1',
+                        original: path.join(process.cwd(), 'fixtures/main.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                    {
+                        name: 'app2',
+                        original: path.join(process.cwd(), 'fixtures/main4.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main4.js'),
+                    },
+                ],
+            ],
+            [
+                'Array of objects with in and out',
+                [
+                    {
+                        in: 'fixtures/main.js',
+                        out: 'outdir/main.js',
+                    },
+                ],
+                [
+                    {
+                        original: 'fixtures/main.js',
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                ],
+            ],
+            ['undefined', undefined, []],
+            [
+                'Array of strings with glob',
+                [path.join(process.cwd(), 'fixtures/*.js')],
+                [
+                    {
+                        original: path.join(process.cwd(), 'fixtures/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main4.js'),
+                    },
+                    {
+                        original: path.join(process.cwd(), 'fixtures/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                ],
+            ],
+            [
+                'Object with entry names with glob',
+                {
+                    app1: path.join(process.cwd(), 'fixtures/*.js'),
+                    app2: path.join(process.cwd(), 'fixtures/**/*.js'),
+                },
+                [
+                    {
+                        name: 'app1',
+                        original: path.join(process.cwd(), 'fixtures/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main4.js'),
+                    },
+                    {
+                        name: 'app1',
+                        original: path.join(process.cwd(), 'fixtures/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                    {
+                        name: 'app2',
+                        original: path.join(process.cwd(), 'fixtures/**/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main4.js'),
+                    },
+                    {
+                        name: 'app2',
+                        original: path.join(process.cwd(), 'fixtures/**/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                    {
+                        name: 'app2',
+                        original: path.join(process.cwd(), 'fixtures/**/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/in/main3.js'),
+                    },
+                    {
+                        name: 'app2',
+                        original: path.join(process.cwd(), 'fixtures/**/*.js'),
+                        resolved: path.join(process.cwd(), 'fixtures/in/main2.js'),
+                    },
+                ],
+            ],
+            [
+                'Array of objects with in and out with globs',
+                [
+                    {
+                        in: 'fixtures/*.js',
+                        out: 'outdir/main.js',
+                    },
+                    {
+                        in: 'fixtures/main4.js',
+                        out: 'outdir/main4.js',
+                    },
+                ],
+                [
+                    {
+                        original: 'fixtures/*.js',
+                        resolved: path.join(process.cwd(), 'fixtures/main4.js'),
+                    },
+                    {
+                        original: 'fixtures/*.js',
+                        resolved: path.join(process.cwd(), 'fixtures/main.js'),
+                    },
+                    {
+                        original: 'fixtures/main4.js',
+                        resolved: path.join(process.cwd(), 'fixtures/main4.js'),
+                    },
+                ],
+            ],
+        ];
+        test.each(expectations)(
+            'Should return the right map of entrynames for "%s".',
+            async (name, entryPoints, entryNames) => {
+                const result = await getEsbuildEntries(
+                    getEsbuildMock({
+                        initialOptions: {
+                            entryPoints,
+                        },
+                    }),
+                    getContextMock(),
+                    mockLogger,
+                );
+                expect(result).toEqual(entryNames);
+            },
+        );
+    });
+
+    describe('doRequest', () => {
+        const getDataStream = () => {
+            const gz = createGzip();
+            const stream = new Readable();
+            stream.push('Some data');
+            stream.push(null);
+            return stream.pipe(gz);
+        };
+
+        const getDataMock = () => ({
+            data: getDataStream(),
+            headers: {
+                'Content-Encoding': 'gzip',
+            },
+        });
+
+        const requestOpts: RequestOpts = {
+            url: INTAKE_URL,
+            method: 'POST',
+            type: 'json',
+            getData: getDataMock,
+        };
+
+        afterEach(() => {
+            nock.cleanAll();
+        });
+
+        test('Should do a request', async () => {
+            const { doRequest } = await import('@dd/core/helpers');
+            const scope = nock(FAKE_URL).post(API_PATH).reply(200, {});
+
+            const response = await doRequest(requestOpts);
+
+            expect(scope.isDone()).toBe(true);
+            expect(response).toEqual({});
+        });
+
+        test('Should retry on error', async () => {
+            const { doRequest } = await import('@dd/core/helpers');
+            // Success after 2 retries.
+            const scope = nock(FAKE_URL)
+                .post(API_PATH)
+                .times(2)
+                .reply(404)
+                .post(API_PATH)
+                .reply(200, { data: 'ok' });
+
+            const response = await doRequest(requestOpts);
+
+            expect(scope.isDone()).toBe(true);
+            expect(response).toEqual({ data: 'ok' });
+        });
+
+        test('Should throw on too many retries', async () => {
+            const { doRequest } = await import('@dd/core/helpers');
+            const scope = nock(FAKE_URL)
+                .post(API_PATH)
+                .times(6)
+                .reply(500, 'Internal Server Error');
+
+            await expect(async () => {
+                await doRequest(requestOpts);
+            }).rejects.toThrow('HTTP 500 Internal Server Error');
+            expect(scope.isDone()).toBe(true);
+        });
+
+        test('Should bail on specific status', async () => {
+            const { doRequest } = await import('@dd/core/helpers');
+            const scope = nock(FAKE_URL).post(API_PATH).reply(400, 'Bad Request');
+
+            await expect(async () => {
+                await doRequest(requestOpts);
+            }).rejects.toThrow('HTTP 400 Bad Request');
+            expect(scope.isDone()).toBe(true);
+        });
+
+        test('Should bail on unrelated errors', async () => {
+            const { doRequest } = await import('@dd/core/helpers');
+            const scope = nock(FAKE_URL).post(API_PATH).reply(404);
+            // Creating the data stream outside should make the fetch invocation fail
+            // on the second pass as it will try to read an already consumed stream.
+            const data = getDataStream();
+
+            await expect(async () => {
+                await doRequest({ ...requestOpts, getData: () => ({ data, headers: {} }) });
+            }).rejects.toThrow('Response body object should not be disturbed or locked');
+            expect(scope.isDone()).toBe(true);
+        });
+
+        test('Should add authentication headers when needed.', async () => {
+            const fetchMock = jest
+                .spyOn(global, 'fetch')
+                .mockImplementation(() => Promise.resolve(new Response('{}')));
+            const { doRequest } = await import('@dd/core/helpers');
+            await doRequest({
+                ...requestOpts,
+                auth: {
+                    apiKey: 'api_key',
+                    appKey: 'app_key',
+                },
+            });
+
+            expect(fetchMock).toHaveBeenCalledWith(
+                INTAKE_URL,
+                expect.objectContaining({
+                    headers: {
+                        // Coming from the getDataMock.
+                        'Content-Encoding': 'gzip',
+                        // Coming from the requestOpts.auth.
+                        'DD-API-KEY': 'api_key',
+                        'DD-APPLICATION-KEY': 'app_key',
+                    },
+                }),
+            );
+        });
+    });
+
+    describe('truncateString', () => {
+        test.each([
+            // No truncation needed.
+            ['Short string', 20, '[...]', 'Short string'],
+            // Keep at least 2 characters on each side.
+            ['Short string', 2, '[...]', 'Sh[...]ng'],
+            // Equaly truncate on both sides.
+            [
+                'A way too long sentence could be truncated a bit.',
+                20,
+                '[...]',
+                'A way t[...]d a bit.',
+            ],
+            // Custom placeholder.
+            [
+                'A way too long sentence could be truncated a bit.',
+                20,
+                '***',
+                'A way to***ed a bit.',
+            ],
+            // Longer sentence.
+            [
+                'Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
+                50,
+                '[...]',
+                'Lorem ipsu[...]t ut labore et dolore magna aliqua.',
+            ],
+        ])(
+            'Should truncate string "%s" to max length %d with placeholder "%s" => "%s"',
+            async (str, maxLength, placeholder, expected) => {
+                const { truncateString } = await import('@dd/core/helpers');
+                expect(truncateString(str, maxLength, placeholder)).toBe(expected);
+            },
+        );
+    });
+});
diff --git a/packages/tests/src/factory/helpers.test.ts b/packages/tests/src/unit/factory/helpers.test.ts
similarity index 89%
rename from packages/tests/src/factory/helpers.test.ts
rename to packages/tests/src/unit/factory/helpers.test.ts
index 890b533f5..7b782a4e6 100644
--- a/packages/tests/src/factory/helpers.test.ts
+++ b/packages/tests/src/unit/factory/helpers.test.ts
@@ -5,7 +5,7 @@
 import type { BuildReport, GlobalContext, Logger, Options, ToInjectItem } from '@dd/core/types';
 import { getContext, getLoggerFactory } from '@dd/factory/helpers';
 import { BUNDLER_VERSIONS } from '@dd/tests/_jest/helpers/constants';
-import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
+import { defaultPluginOptions, getMockBuild } from '@dd/tests/_jest/helpers/mocks';
 import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
 import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
 import stripAnsi from 'strip-ansi';
@@ -71,7 +71,7 @@ describe('Factory Helpers', () => {
         });
 
         test('Should inject items for the injection plugin.', () => {
-            const injections: ToInjectItem[] = [];
+            const injections: Map<string, ToInjectItem> = new Map();
             const context = getContext({
                 options: defaultPluginOptions,
                 bundlerName: 'webpack',
@@ -81,13 +81,13 @@ describe('Factory Helpers', () => {
             });
             const injectedItem: ToInjectItem = { type: 'code', value: 'injected' };
             context.inject(injectedItem);
-            expect(injections).toEqual([injectedItem]);
+            expect(Array.from(injections.entries())).toEqual([[expect.any(String), injectedItem]]);
         });
     });
 
     describe('getLoggerFactory', () => {
         const setupLogger = (name: string): [Logger, BuildReport] => {
-            const mockBuild = { errors: [], warnings: [], logs: [] };
+            const mockBuild = getMockBuild();
             const loggerFactory = getLoggerFactory(mockBuild, 'debug');
             const logger = loggerFactory(name);
 
@@ -111,41 +111,42 @@ describe('Factory Helpers', () => {
 
         const assessLogs = (name: string) => {
             expect(logMock).toHaveBeenCalledTimes(2);
-            expect(getOutput(logMock, 0)).toBe(`[info|${name}] An info message.`);
-            expect(getOutput(logMock, 1)).toBe(`[debug|${name}] A debug message.`);
+            expect(getOutput(logMock, 0)).toBe(`[info|esbuild|${name}] An info message.`);
+            expect(getOutput(logMock, 1)).toBe(`[debug|esbuild|${name}] A debug message.`);
 
             expect(errorMock).toHaveBeenCalledTimes(1);
-            expect(getOutput(errorMock, 0)).toBe(`[error|${name}] An error occurred.`);
+            expect(getOutput(errorMock, 0)).toBe(`[error|esbuild|${name}] An error occurred.`);
 
             expect(warnMock).toHaveBeenCalledTimes(1);
-            expect(getOutput(warnMock, 0)).toBe(`[warn|${name}] A warning message.`);
+            expect(getOutput(warnMock, 0)).toBe(`[warn|esbuild|${name}] A warning message.`);
         };
 
         const assessReport = (name: string, buildReport: BuildReport) => {
             expect(buildReport.logs).toHaveLength(4);
-            expect(buildReport.logs[0]).toEqual({
+            const baseLog = {
+                bundler: 'esbuild',
                 pluginName: name,
+                time: expect.any(Number),
+            };
+            expect(buildReport.logs[0]).toEqual({
+                ...baseLog,
                 type: 'error',
                 message: 'An error occurred.',
-                time: expect.any(Number),
             });
             expect(buildReport.logs[1]).toEqual({
-                pluginName: name,
+                ...baseLog,
                 type: 'warn',
                 message: 'A warning message.',
-                time: expect.any(Number),
             });
             expect(buildReport.logs[2]).toEqual({
-                pluginName: name,
+                ...baseLog,
                 type: 'info',
                 message: 'An info message.',
-                time: expect.any(Number),
             });
             expect(buildReport.logs[3]).toEqual({
-                pluginName: name,
+                ...baseLog,
                 type: 'debug',
                 message: 'A debug message.',
-                time: expect.any(Number),
             });
 
             expect(buildReport.errors).toEqual(['An error occurred.']);
diff --git a/packages/tests/src/factory/index.test.ts b/packages/tests/src/unit/factory/index.test.ts
similarity index 100%
rename from packages/tests/src/factory/index.test.ts
rename to packages/tests/src/unit/factory/index.test.ts
diff --git a/packages/tests/src/plugins/build-report/helpers.test.ts b/packages/tests/src/unit/plugins/build-report/helpers.test.ts
similarity index 100%
rename from packages/tests/src/plugins/build-report/helpers.test.ts
rename to packages/tests/src/unit/plugins/build-report/helpers.test.ts
diff --git a/packages/tests/src/plugins/build-report/index.test.ts b/packages/tests/src/unit/plugins/build-report/index.test.ts
similarity index 92%
rename from packages/tests/src/plugins/build-report/index.test.ts
rename to packages/tests/src/unit/plugins/build-report/index.test.ts
index 9249b0f87..ef9bf1b96 100644
--- a/packages/tests/src/plugins/build-report/index.test.ts
+++ b/packages/tests/src/unit/plugins/build-report/index.test.ts
@@ -2,7 +2,7 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { getResolvedPath } from '@dd/core/helpers';
+import { serializeBuildReport, unserializeBuildReport } from '@dd/core/helpers';
 import type {
     Input,
     Entry,
@@ -12,21 +12,20 @@ import type {
     BuildReport,
     SerializedInput,
 } from '@dd/core/types';
-import {
-    serializeBuildReport,
-    unserializeBuildReport,
-} from '@dd/internal-build-report-plugin/helpers';
 import { generateProject } from '@dd/tests/_jest/helpers/generateMassiveProject';
 import {
-    debugFilesPlugins,
     defaultEntry,
     defaultPluginOptions,
     filterOutParticularities,
     getComplexBuildOverrides,
 } from '@dd/tests/_jest/helpers/mocks';
 import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
-import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
-import { getWebpack4Entries } from '@dd/tests/_jest/helpers/xpackConfigs';
+import type {
+    BundlerOptionsOverrides,
+    CleanupEverythingFn,
+    CleanupFn,
+} from '@dd/tests/_jest/helpers/types';
+import { debugFilesPlugins } from '@dd/tools/helpers';
 import path from 'path';
 
 const sortFiles = (a: File | Output | Entry, b: File | Output | Entry) => {
@@ -64,7 +63,7 @@ describe('Build Report Plugin', () => {
     describe('Basic build', () => {
         const bundlerOutdir: Record<string, string> = {};
         const buildReports: Record<string, BuildReport> = {};
-        let cleanup: CleanupFn;
+        let cleanup: CleanupEverythingFn;
 
         beforeAll(async () => {
             cleanup = await runBundlers(getPluginConfig(bundlerOutdir, buildReports));
@@ -76,8 +75,8 @@ describe('Build Report Plugin', () => {
 
         const expectedInput = () =>
             expect.objectContaining<Input>({
-                name: `src/_jest/fixtures/main.js`,
-                filepath: getResolvedPath(defaultEntry),
+                name: `easy_project/main.js`,
+                filepath: path.resolve(cleanup.workingDir, defaultEntry),
                 dependencies: new Set(),
                 dependents: new Set(),
                 size: 302,
@@ -90,8 +89,8 @@ describe('Build Report Plugin', () => {
                 filepath: path.join(outDir, 'main.js'),
                 inputs: [
                     expect.objectContaining<Input>({
-                        name: `src/_jest/fixtures/main.js`,
-                        filepath: getResolvedPath(defaultEntry),
+                        name: `easy_project/main.js`,
+                        filepath: path.resolve(cleanup.workingDir, defaultEntry),
                         dependencies: new Set(),
                         dependents: new Set(),
                         size: expect.any(Number),
@@ -179,7 +178,7 @@ describe('Build Report Plugin', () => {
         // Intercept contexts to verify it at the moment they're used.
         const bundlerOutdir: Record<string, string> = {};
         const buildReports: Record<string, BuildReport> = {};
-        let cleanup: CleanupFn;
+        let cleanup: CleanupEverythingFn;
 
         beforeAll(async () => {
             cleanup = await runBundlers(
@@ -194,8 +193,8 @@ describe('Build Report Plugin', () => {
 
         const expectedInput = (name: string) =>
             expect.objectContaining<SerializedInput>({
-                name: `src/_jest/fixtures/project/${name}.js`,
-                filepath: path.join(process.cwd(), `src/_jest/fixtures/project/${name}.js`),
+                name: `hard_project/${name}.js`,
+                filepath: path.join(cleanup.workingDir, `hard_project/${name}.js`),
                 dependencies: expect.any(Array),
                 dependents: [],
                 size: expect.any(Number),
@@ -227,12 +226,12 @@ describe('Build Report Plugin', () => {
                         'color-convert/route.js',
                         'color-name/index.js',
                         'escape-string-regexp/index.js',
-                        'src/_jest/fixtures/project/main1.js',
-                        'src/_jest/fixtures/project/main2.js',
-                        'src/_jest/fixtures/project/src/srcFile0.js',
-                        'src/_jest/fixtures/project/src/srcFile1.js',
-                        'src/_jest/fixtures/project/workspaces/app/workspaceFile0.js',
-                        'src/_jest/fixtures/project/workspaces/app/workspaceFile1.js',
+                        'hard_project/main1.js',
+                        'hard_project/main2.js',
+                        'hard_project/src/srcFile0.js',
+                        'hard_project/src/srcFile1.js',
+                        'hard_project/workspaces/app/workspaceFile0.js',
+                        'hard_project/workspaces/app/workspaceFile1.js',
                         'supports-color/browser.js',
                     ]);
                 });
@@ -269,7 +268,7 @@ describe('Build Report Plugin', () => {
                         .sort(sortFiles);
 
                     const entryFiles = inputs.filter((file) =>
-                        file.name.startsWith('src/_jest/fixtures/project/main'),
+                        file.name.startsWith('hard_project/main'),
                     );
 
                     expect(entryFiles).toEqual([expectedInput('main1'), expectedInput('main2')]);
@@ -277,19 +276,19 @@ describe('Build Report Plugin', () => {
 
                 test.each([
                     {
-                        filename: 'src/_jest/fixtures/project/main1.js',
+                        filename: 'hard_project/main1.js',
                         dependencies: [
                             'chalk/index.js',
-                            'src/_jest/fixtures/project/src/srcFile0.js',
-                            'src/_jest/fixtures/project/workspaces/app/workspaceFile1.js',
+                            'hard_project/src/srcFile0.js',
+                            'hard_project/workspaces/app/workspaceFile1.js',
                         ],
                         dependents: [],
                     },
                     {
-                        filename: 'src/_jest/fixtures/project/main2.js',
+                        filename: 'hard_project/main2.js',
                         dependencies: [
-                            'src/_jest/fixtures/project/src/srcFile0.js',
-                            'src/_jest/fixtures/project/src/srcFile1.js',
+                            'hard_project/src/srcFile0.js',
+                            'hard_project/src/srcFile1.js',
                         ],
                         dependents: [],
                     },
@@ -308,7 +307,7 @@ describe('Build Report Plugin', () => {
                             'supports-color/browser.js',
                         ],
                         // It should also have a single dependent which is main1.
-                        dependents: ['src/_jest/fixtures/project/main1.js'],
+                        dependents: ['hard_project/main1.js'],
                     },
                     {
                         filename: 'color-convert/route.js',
@@ -553,7 +552,7 @@ describe('Build Report Plugin', () => {
 
         beforeAll(async () => {
             const entries = await generateProject(2, 500);
-            const bundlerOverrides = {
+            const bundlerOverrides: BundlerOptionsOverrides = {
                 rollup: {
                     input: entries,
                 },
@@ -565,10 +564,7 @@ describe('Build Report Plugin', () => {
                 },
                 // Mode production makes the build waaaaayyyyy too slow.
                 webpack5: { mode: 'none', entry: entries },
-                webpack4: {
-                    mode: 'none',
-                    entry: getWebpack4Entries(entries),
-                },
+                webpack4: { mode: 'none', entry: entries },
             };
             cleanup = await runBundlers(
                 getPluginConfig(bundlerOutdir, buildReports, { logLevel: 'error', telemetry: {} }),
diff --git a/packages/tests/src/plugins/bundler-report/index.test.ts b/packages/tests/src/unit/plugins/bundler-report/index.test.ts
similarity index 76%
rename from packages/tests/src/plugins/bundler-report/index.test.ts
rename to packages/tests/src/unit/plugins/bundler-report/index.test.ts
index bfedc1fc2..44b25101b 100644
--- a/packages/tests/src/plugins/bundler-report/index.test.ts
+++ b/packages/tests/src/unit/plugins/bundler-report/index.test.ts
@@ -2,15 +2,16 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import type { BundlerReport, Options } from '@dd/core/types';
-import { defaultDestination, defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
+import type { BundlerReport, GlobalContext, Options } from '@dd/core/types';
+import { defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
 import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
-import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
+import type { CleanupEverythingFn } from '@dd/tests/_jest/helpers/types';
 
 describe('Bundler Report', () => {
     // Intercept contexts to verify it at the moment they're used.
     const bundlerReports: Record<string, BundlerReport> = {};
-    let cleanup: CleanupFn;
+    const contexts: Record<string, Partial<GlobalContext>> = {};
+    let cleanup: CleanupEverythingFn;
     beforeAll(async () => {
         const pluginConfig: Options = {
             ...defaultPluginOptions,
@@ -22,6 +23,9 @@ describe('Bundler Report', () => {
                         name: 'custom-plugin',
                         writeBundle() {
                             const config = context.bundler.rawConfig;
+                            contexts[bundlerName] = {
+                                cwd: context.cwd,
+                            };
                             bundlerReports[bundlerName] = JSON.parse(
                                 JSON.stringify({
                                     ...context.bundler,
@@ -48,7 +52,7 @@ describe('Bundler Report', () => {
             const report = bundlerReports[name];
             const outDir = report.outDir;
 
-            const expectedOutDir = new RegExp(`^${defaultDestination}/[^/]+/${name}$`);
+            const expectedOutDir = new RegExp(`^${cleanup.workingDir}/[^/]+/${name}$`);
 
             expect(outDir).toMatch(expectedOutDir);
         });
@@ -59,5 +63,9 @@ describe('Bundler Report', () => {
             expect(rawConfig).toBeDefined();
             expect(rawConfig).toEqual(expect.any(Object));
         });
+
+        test('Should have the right cwd.', () => {
+            expect(contexts[name].cwd).toBe(cleanup.workingDir);
+        });
     });
 });
diff --git a/packages/tests/src/plugins/rum/index.test.ts b/packages/tests/src/unit/plugins/error-tracking/index.test.ts
similarity index 83%
rename from packages/tests/src/plugins/rum/index.test.ts
rename to packages/tests/src/unit/plugins/error-tracking/index.test.ts
index e36170705..59585a6be 100644
--- a/packages/tests/src/plugins/rum/index.test.ts
+++ b/packages/tests/src/unit/plugins/error-tracking/index.test.ts
@@ -2,13 +2,13 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { uploadSourcemaps } from '@dd/rum-plugin/sourcemaps/index';
+import { uploadSourcemaps } from '@dd/error-tracking-plugin/sourcemaps/index';
 import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
 import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
 
 import { getSourcemapsConfiguration } from './testHelpers';
 
-jest.mock('@dd/rum-plugin/sourcemaps/index', () => {
+jest.mock('@dd/error-tracking-plugin/sourcemaps/index', () => {
     return {
         uploadSourcemaps: jest.fn(),
     };
@@ -16,7 +16,7 @@ jest.mock('@dd/rum-plugin/sourcemaps/index', () => {
 
 const uploadSourcemapsMock = jest.mocked(uploadSourcemaps);
 
-describe('RUM Plugin', () => {
+describe('Error Tracking Plugin', () => {
     const cleanups: CleanupFn[] = [];
 
     afterAll(async () => {
@@ -26,7 +26,7 @@ describe('RUM Plugin', () => {
     test('Should process the sourcemaps if enabled.', async () => {
         cleanups.push(
             await runBundlers({
-                rum: {
+                errorTracking: {
                     sourcemaps: getSourcemapsConfiguration(),
                 },
             }),
@@ -37,7 +37,7 @@ describe('RUM Plugin', () => {
     test('Should not process the sourcemaps with no options.', async () => {
         cleanups.push(
             await runBundlers({
-                rum: {},
+                errorTracking: {},
             }),
         );
 
diff --git a/packages/tests/src/plugins/rum/sourcemaps/files.test.ts b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/files.test.ts
similarity index 89%
rename from packages/tests/src/plugins/rum/sourcemaps/files.test.ts
rename to packages/tests/src/unit/plugins/error-tracking/sourcemaps/files.test.ts
index 7bb1e1aa9..48d0f808f 100644
--- a/packages/tests/src/plugins/rum/sourcemaps/files.test.ts
+++ b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/files.test.ts
@@ -2,13 +2,13 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { getSourcemapsFiles } from '@dd/rum-plugin/sourcemaps/files';
-import { getContextMock } from '@dd/tests/_jest/helpers/mocks';
+import { getSourcemapsFiles } from '@dd/error-tracking-plugin/sourcemaps/files';
+import { getContextMock, getMockBuild } from '@dd/tests/_jest/helpers/mocks';
 import path from 'path';
 
 import { getSourcemapsConfiguration } from '../testHelpers';
 
-describe('RUM Plugin Sourcemaps Files', () => {
+describe('Error Tracking Plugin Sourcemaps Files', () => {
     test('Should get sourcemap files.', async () => {
         const sourcemaps = getSourcemapsFiles(
             getSourcemapsConfiguration({
@@ -22,9 +22,7 @@ describe('RUM Plugin Sourcemaps Files', () => {
                     version: '1.0.0',
                 },
                 build: {
-                    warnings: [],
-                    errors: [],
-                    logs: [],
+                    ...getMockBuild(),
                     outputs: [
                         'fixtures/common.js',
                         'fixtures/common.min.js.map',
diff --git a/packages/tests/src/plugins/rum/sourcemaps/payload.test.ts b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/payload.test.ts
similarity index 96%
rename from packages/tests/src/plugins/rum/sourcemaps/payload.test.ts
rename to packages/tests/src/unit/plugins/error-tracking/sourcemaps/payload.test.ts
index b651d2844..4a84d6934 100644
--- a/packages/tests/src/plugins/rum/sourcemaps/payload.test.ts
+++ b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/payload.test.ts
@@ -2,7 +2,7 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import { checkFile, getPayload, prefixRepeat } from '@dd/rum-plugin/sourcemaps/payload';
+import { checkFile, getPayload, prefixRepeat } from '@dd/error-tracking-plugin/sourcemaps/payload';
 import { vol } from 'memfs';
 import path from 'path';
 
@@ -10,7 +10,7 @@ import { getMetadataMock, getRepositoryDataMock, getSourcemapMock } from '../tes
 
 jest.mock('fs', () => require('memfs').fs);
 
-describe('RUM Plugins Sourcemaps Payloads', () => {
+describe('Error Tracking Plugins Sourcemaps Payloads', () => {
     describe('prefixRepeat', () => {
         test.each([
             { prefix: '/testing/path/to', filePath: '/path/to/file.js', expected: 'path/to' },
diff --git a/packages/tests/src/plugins/rum/sourcemaps/sender.test.ts b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/sender.test.ts
similarity index 97%
rename from packages/tests/src/plugins/rum/sourcemaps/sender.test.ts
rename to packages/tests/src/unit/plugins/error-tracking/sourcemaps/sender.test.ts
index 20660dfb0..013fb29e4 100644
--- a/packages/tests/src/plugins/rum/sourcemaps/sender.test.ts
+++ b/packages/tests/src/unit/plugins/error-tracking/sourcemaps/sender.test.ts
@@ -3,7 +3,7 @@
 // Copyright 2019-Present Datadog, Inc.
 
 import { doRequest } from '@dd/core/helpers';
-import { getData, sendSourcemaps, upload } from '@dd/rum-plugin/sourcemaps/sender';
+import { getData, sendSourcemaps, upload } from '@dd/error-tracking-plugin/sourcemaps/sender';
 import { getContextMock, mockLogFn, mockLogger } from '@dd/tests/_jest/helpers/mocks';
 import { vol } from 'memfs';
 import { type Stream } from 'stream';
@@ -36,7 +36,7 @@ function readFully(stream: Stream): Promise<Buffer> {
     });
 }
 
-describe('RUM Plugin Sourcemaps', () => {
+describe('Error Tracking Plugin Sourcemaps', () => {
     describe('getData', () => {
         afterEach(() => {
             vol.reset();
diff --git a/packages/tests/src/plugins/rum/testHelpers.ts b/packages/tests/src/unit/plugins/error-tracking/testHelpers.ts
similarity index 83%
rename from packages/tests/src/plugins/rum/testHelpers.ts
rename to packages/tests/src/unit/plugins/error-tracking/testHelpers.ts
index 3242f6cfb..3860e68c3 100644
--- a/packages/tests/src/plugins/rum/testHelpers.ts
+++ b/packages/tests/src/unit/plugins/error-tracking/testHelpers.ts
@@ -3,29 +3,33 @@
 // Copyright 2019-Present Datadog, Inc.
 
 import type { RepositoryData } from '@dd/core/types';
-import { TrackedFilesMatcher } from '@dd/internal-git-plugin/trackedFilesMatcher';
-import type { Metadata, MultipartValue, Payload } from '@dd/rum-plugin/sourcemaps/payload';
 import type {
-    RumSourcemapsOptions,
-    RumSourcemapsOptionsWithDefaults,
+    Metadata,
+    MultipartValue,
+    Payload,
+} from '@dd/error-tracking-plugin/sourcemaps/payload';
+import type {
+    SourcemapsOptions,
+    SourcemapsOptionsWithDefaults,
     Sourcemap,
-} from '@dd/rum-plugin/types';
+} from '@dd/error-tracking-plugin/types';
+import { TrackedFilesMatcher } from '@dd/internal-git-plugin/trackedFilesMatcher';
 import { INTAKE_URL } from '@dd/tests/_jest/helpers/mocks';
 
 export const getMinimalSourcemapsConfiguration = (
-    options: Partial<RumSourcemapsOptions> = {},
-): RumSourcemapsOptions => {
+    options: Partial<SourcemapsOptions> = {},
+): SourcemapsOptions => {
     return {
         minifiedPathPrefix: '/prefix',
         releaseVersion: '1.0.0',
-        service: 'rum-build-plugin-sourcemaps',
+        service: 'error-tracking-build-plugin-sourcemaps',
         ...options,
     };
 };
 
 export const getSourcemapsConfiguration = (
-    options: Partial<RumSourcemapsOptions> = {},
-): RumSourcemapsOptionsWithDefaults => {
+    options: Partial<SourcemapsOptions> = {},
+): SourcemapsOptionsWithDefaults => {
     return {
         bailOnError: false,
         dryRun: false,
@@ -33,7 +37,7 @@ export const getSourcemapsConfiguration = (
         intakeUrl: INTAKE_URL,
         minifiedPathPrefix: '/prefix',
         releaseVersion: '1.0.0',
-        service: 'rum-build-plugin-sourcemaps',
+        service: 'error-tracking-build-plugin-sourcemaps',
         ...options,
     };
 };
@@ -53,7 +57,7 @@ export const getMetadataMock = (options: Partial<Metadata> = {}): Metadata => {
     return {
         plugin_version: '1.0.0',
         project_path: '/path/to/project',
-        service: 'rum-build-plugin-sourcemaps',
+        service: 'error-tracking-build-plugin-sourcemaps',
         type: 'js_sourcemap',
         version: '1.0.0',
         ...options,
diff --git a/packages/tests/src/plugins/rum/validate.test.ts b/packages/tests/src/unit/plugins/error-tracking/validate.test.ts
similarity index 86%
rename from packages/tests/src/plugins/rum/validate.test.ts
rename to packages/tests/src/unit/plugins/error-tracking/validate.test.ts
index c12f12777..2eac6ddcd 100644
--- a/packages/tests/src/plugins/rum/validate.test.ts
+++ b/packages/tests/src/unit/plugins/error-tracking/validate.test.ts
@@ -2,14 +2,14 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import type { RumSourcemapsOptions } from '@dd/rum-plugin/types';
-import { validateOptions, validateSourcemapsOptions } from '@dd/rum-plugin/validate';
+import type { SourcemapsOptions } from '@dd/error-tracking-plugin/types';
+import { validateOptions, validateSourcemapsOptions } from '@dd/error-tracking-plugin/validate';
 import { mockLogger } from '@dd/tests/_jest/helpers/mocks';
 import stripAnsi from 'strip-ansi';
 
 import { getMinimalSourcemapsConfiguration } from './testHelpers';
 
-describe('RUM Plugins validate', () => {
+describe('Error Tracking Plugins validate', () => {
     describe('validateOptions', () => {
         test('Should return the validated configuration', () => {
             const config = validateOptions(
@@ -17,7 +17,7 @@ describe('RUM Plugins validate', () => {
                     auth: {
                         apiKey: '123',
                     },
-                    rum: {
+                    errorTracking: {
                         disabled: false,
                     },
                 },
@@ -36,9 +36,9 @@ describe('RUM Plugins validate', () => {
                         auth: {
                             apiKey: '123',
                         },
-                        rum: {
+                        errorTracking: {
                             // Invalid configuration, missing required fields.
-                            sourcemaps: {} as RumSourcemapsOptions,
+                            sourcemaps: {} as SourcemapsOptions,
                         },
                     },
                     mockLogger,
@@ -49,8 +49,8 @@ describe('RUM Plugins validate', () => {
     describe('validateSourcemapsOptions', () => {
         test('Should return errors for each missing required field', () => {
             const { errors } = validateSourcemapsOptions({
-                rum: {
-                    sourcemaps: {} as RumSourcemapsOptions,
+                errorTracking: {
+                    sourcemaps: {} as SourcemapsOptions,
                 },
             });
 
@@ -64,14 +64,14 @@ describe('RUM Plugins validate', () => {
         });
 
         test('Should return the validated configuration with defaults', () => {
-            const configObject: RumSourcemapsOptions = {
+            const configObject: SourcemapsOptions = {
                 minifiedPathPrefix: '/path/to/minified',
                 releaseVersion: '1.0.0',
                 service: 'service',
             };
 
             const { config, errors } = validateSourcemapsOptions({
-                rum: {
+                errorTracking: {
                     sourcemaps: getMinimalSourcemapsConfiguration(configObject),
                 },
             });
@@ -88,10 +88,9 @@ describe('RUM Plugins validate', () => {
 
         test('Should return an error with a bad minifiedPathPrefix', () => {
             const { errors } = validateSourcemapsOptions({
-                rum: {
+                errorTracking: {
                     sourcemaps: getMinimalSourcemapsConfiguration({
-                        minifiedPathPrefix:
-                            'bad-prefix' as RumSourcemapsOptions['minifiedPathPrefix'],
+                        minifiedPathPrefix: 'bad-prefix' as SourcemapsOptions['minifiedPathPrefix'],
                     }),
                 },
             });
@@ -104,7 +103,7 @@ describe('RUM Plugins validate', () => {
 
         test('Should default to the expected intake url', () => {
             const { config } = validateSourcemapsOptions({
-                rum: {
+                errorTracking: {
                     sourcemaps: getMinimalSourcemapsConfiguration(),
                 },
             });
@@ -114,7 +113,7 @@ describe('RUM Plugins validate', () => {
 
         test('Should use the provided configuration as the intake url', () => {
             const { config } = validateSourcemapsOptions({
-                rum: {
+                errorTracking: {
                     sourcemaps: getMinimalSourcemapsConfiguration({
                         intakeUrl: 'https://example.com',
                     }),
@@ -128,7 +127,7 @@ describe('RUM Plugins validate', () => {
             const initialEnvValue = process.env.DATADOG_SOURCEMAP_INTAKE_URL;
             process.env.DATADOG_SOURCEMAP_INTAKE_URL = 'https://example.com';
             const { config } = validateSourcemapsOptions({
-                rum: {
+                errorTracking: {
                     sourcemaps: getMinimalSourcemapsConfiguration(),
                 },
             });
diff --git a/packages/tests/src/plugins/git/helpers.test.ts b/packages/tests/src/unit/plugins/git/helpers.test.ts
similarity index 100%
rename from packages/tests/src/plugins/git/helpers.test.ts
rename to packages/tests/src/unit/plugins/git/helpers.test.ts
diff --git a/packages/tests/src/plugins/git/index.test.ts b/packages/tests/src/unit/plugins/git/index.test.ts
similarity index 87%
rename from packages/tests/src/plugins/git/index.test.ts
rename to packages/tests/src/unit/plugins/git/index.test.ts
index a63b6b42f..c308bafca 100644
--- a/packages/tests/src/plugins/git/index.test.ts
+++ b/packages/tests/src/unit/plugins/git/index.test.ts
@@ -2,14 +2,14 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import type { RepositoryData } from '@dd/core/types';
+import type { Options, RepositoryData } from '@dd/core/types';
+import { uploadSourcemaps } from '@dd/error-tracking-plugin/sourcemaps/index';
 import { getRepositoryData } from '@dd/internal-git-plugin/helpers';
 import { TrackedFilesMatcher } from '@dd/internal-git-plugin/trackedFilesMatcher';
-import { uploadSourcemaps } from '@dd/rum-plugin/sourcemaps/index';
 import { API_PATH, FAKE_URL, defaultPluginOptions } from '@dd/tests/_jest/helpers/mocks';
 import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
 import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
-import { getSourcemapsConfiguration } from '@dd/tests/plugins/rum/testHelpers';
+import { getSourcemapsConfiguration } from '@dd/tests/unit/plugins/error-tracking/testHelpers';
 import nock from 'nock';
 
 jest.mock('@dd/internal-git-plugin/helpers', () => {
@@ -20,8 +20,8 @@ jest.mock('@dd/internal-git-plugin/helpers', () => {
     };
 });
 
-jest.mock('@dd/rum-plugin/sourcemaps/index', () => {
-    const originalModule = jest.requireActual('@dd/rum-plugin/sourcemaps/index');
+jest.mock('@dd/error-tracking-plugin/sourcemaps/index', () => {
+    const originalModule = jest.requireActual('@dd/error-tracking-plugin/sourcemaps/index');
     return {
         ...originalModule,
         uploadSourcemaps: jest.fn(),
@@ -55,9 +55,9 @@ describe('Git Plugin', () => {
         let nbCallsToGetRepositoryData = 0;
         let cleanup: CleanupFn;
         beforeAll(async () => {
-            const pluginConfig = {
+            const pluginConfig: Options = {
                 ...defaultPluginOptions,
-                rum: {
+                errorTracking: {
                     // We need sourcemaps to trigger the git plugin.
                     sourcemaps: getSourcemapsConfiguration(),
                 },
@@ -110,9 +110,9 @@ describe('Git Plugin', () => {
         });
 
         test('Should not run if we disable it from the configuration', async () => {
-            const pluginConfig = {
+            const pluginConfig: Options = {
                 ...defaultPluginOptions,
-                rum: {
+                errorTracking: {
                     sourcemaps: getSourcemapsConfiguration(),
                 },
                 disableGit: true,
diff --git a/packages/tests/src/plugins/git/trackedFilesMatcher.test.ts b/packages/tests/src/unit/plugins/git/trackedFilesMatcher.test.ts
similarity index 100%
rename from packages/tests/src/plugins/git/trackedFilesMatcher.test.ts
rename to packages/tests/src/unit/plugins/git/trackedFilesMatcher.test.ts
diff --git a/packages/tests/src/plugins/injection/helpers.test.ts b/packages/tests/src/unit/plugins/injection/helpers.test.ts
similarity index 77%
rename from packages/tests/src/plugins/injection/helpers.test.ts
rename to packages/tests/src/unit/plugins/injection/helpers.test.ts
index 76ce46203..e7aec9b24 100644
--- a/packages/tests/src/plugins/injection/helpers.test.ts
+++ b/packages/tests/src/unit/plugins/injection/helpers.test.ts
@@ -2,12 +2,13 @@
 // This product includes software developed at Datadog (https://www.datadoghq.com/).
 // Copyright 2019-Present Datadog, Inc.
 
-import type { ToInjectItem } from '@dd/core/types';
+import { InjectPosition, type ToInjectItem } from '@dd/core/types';
 import {
     processInjections,
     processItem,
     processLocalFile,
     processDistantFile,
+    getInjectedValue,
 } from '@dd/internal-injection-plugin/helpers';
 import { mockLogger } from '@dd/tests/_jest/helpers/mocks';
 import { vol } from 'memfs';
@@ -39,13 +40,13 @@ const nonExistingDistantFile: ToInjectItem = {
 describe('Injection Plugin Helpers', () => {
     let nockScope: nock.Scope;
 
-    beforeEach(() => {
+    beforeEach(async () => {
         nockScope = nock('https://example.com')
             .get('/distant-file.js')
             .reply(200, distantFileContent);
         // Emulate some fixtures.
         vol.fromJSON({
-            [existingFile.value]: localFileContent,
+            [await getInjectedValue(existingFile)]: localFileContent,
         });
     });
 
@@ -55,18 +56,28 @@ describe('Injection Plugin Helpers', () => {
 
     describe('processInjections', () => {
         test('Should process injections without throwing.', async () => {
-            const items: ToInjectItem[] = [
-                code,
-                existingFile,
-                nonExistingFile,
-                existingDistantFile,
-                nonExistingDistantFile,
-            ];
+            const items: Map<string, ToInjectItem> = new Map([
+                ['code', code],
+                ['existingFile', existingFile],
+                ['nonExistingFile', nonExistingFile],
+                ['existingDistantFile', existingDistantFile],
+                ['nonExistingDistantFile', nonExistingDistantFile],
+            ]);
 
-            const expectResult = expect(processInjections(items, mockLogger)).resolves;
+            const prom = processInjections(items, mockLogger);
+            const expectResult = expect(prom).resolves;
 
             await expectResult.not.toThrow();
-            await expectResult.toEqual([codeContent, localFileContent, distantFileContent]);
+
+            const results = await prom;
+            expect(Array.from(results.entries())).toEqual([
+                ['code', { position: InjectPosition.BEFORE, value: codeContent }],
+                ['existingFile', { position: InjectPosition.BEFORE, value: localFileContent }],
+                [
+                    'existingDistantFile',
+                    { position: InjectPosition.BEFORE, value: distantFileContent },
+                ],
+            ]);
 
             expect(nockScope.isDone()).toBe(true);
         });
@@ -139,8 +150,7 @@ describe('Injection Plugin Helpers', () => {
                 expectation: localFileContent,
             },
         ])('Should process local file $description.', async ({ value, expectation }) => {
-            const item: ToInjectItem = { type: 'file', value };
-            const expectResult = expect(processLocalFile(item)).resolves;
+            const expectResult = expect(processLocalFile(value)).resolves;
 
             await expectResult.not.toThrow();
             await expectResult.toEqual(expectation);
@@ -154,12 +164,9 @@ describe('Injection Plugin Helpers', () => {
                 .delay(10)
                 .reply(200, 'delayed distant file content');
 
-            const item: ToInjectItem = {
-                type: 'file',
-                value: 'https://example.com/delayed-distant-file.js',
-            };
-
-            await expect(processDistantFile(item, 1)).rejects.toThrow('Timeout');
+            await expect(
+                processDistantFile('https://example.com/delayed-distant-file.js', 1),
+            ).rejects.toThrow('Timeout');
         });
     });
 });
diff --git a/packages/tests/src/unit/plugins/injection/index.test.ts b/packages/tests/src/unit/plugins/injection/index.test.ts
new file mode 100644
index 000000000..172af7ac4
--- /dev/null
+++ b/packages/tests/src/unit/plugins/injection/index.test.ts
@@ -0,0 +1,405 @@
+// 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 { outputFileSync } from '@dd/core/helpers';
+import type { Assign, BundlerFullName, Options, ToInjectItem } from '@dd/core/types';
+import { InjectPosition } from '@dd/core/types';
+import { AFTER_INJECTION, BEFORE_INJECTION } from '@dd/internal-injection-plugin/constants';
+import { getComplexBuildOverrides, getNodeSafeBuildOverrides } from '@dd/tests/_jest/helpers/mocks';
+import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
+import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
+import { header, licenses } from '@dd/tools/commands/oss/templates';
+import { debugFilesPlugins, execute } from '@dd/tools/helpers';
+import { readFileSync, writeFileSync } from 'fs';
+import { glob } from 'glob';
+import nock from 'nock';
+import path from 'path';
+
+const FAKE_FILE_PREFIX = 'fake-file-to-inject-';
+// Files that we will execute part of the test.
+const FILES = ['main.js', 'app1.js', 'app2.js'] as const;
+const DOMAIN = 'https://example.com';
+
+type ExpectedValues = [string | RegExp, number | [number, number]];
+type BaseExpectation = {
+    name: string;
+    logs?: Record<File, ExpectedValues>;
+    content: ExpectedValues;
+};
+type EasyExpectation = Assign<BaseExpectation, { logs?: { 'main.js': ExpectedValues } }>;
+type HardExpectation = Assign<
+    BaseExpectation,
+    { logs?: { 'app1.js': ExpectedValues; 'app2.js': ExpectedValues } }
+>;
+type BuildState = {
+    outdir?: string;
+    content?: string;
+    // Separate logs based on executed file.
+    logs?: Partial<Record<File, string>>;
+};
+type File = (typeof FILES)[number];
+enum ContentType {
+    CODE = 'code',
+    LOCAL = 'local file',
+    DISTANT = 'distant file',
+}
+enum Position {
+    BEFORE = 'before',
+    MIDDLE = 'middle',
+    AFTER = 'after',
+}
+
+const getLog = (type: ContentType, position: Position) => {
+    const positionString = `in ${position}`;
+    const contentString = `Hello injection from ${type}`;
+    return `${contentString} ${positionString}.`;
+};
+
+const getContent = (type: ContentType, position: Position) => {
+    return `console.log("${getLog(type, position)}");`;
+};
+
+const getFileUrl = (position: Position) => {
+    return `/${FAKE_FILE_PREFIX}${position}.js`;
+};
+
+const escapeStringForRegExp = (str: string) =>
+    str
+        // Escape sensible chars in RegExps.
+        .replace(/([().[\]])/g, '\\$1')
+        // Replace quotes to allow for both single and double quotes.
+        .replace(/["']/g, `(?:"|')`);
+
+describe('Injection Plugin', () => {
+    // This is the string we log in our entry files
+    // easy_project/src/main.js and hard_project/src/main1.js.
+    const normalLog = 'Hello World!';
+
+    // Prepare a special injection where we use imports in MIDDLE.
+    const specialLog: string = 'Hello injection with colors from code in middle.';
+
+    // List of expectations for each type of tests.
+    const noMarkers: BaseExpectation[] = [
+        {
+            name: 'No BEFORE_INJECTION markers in easy build',
+            content: [BEFORE_INJECTION, 0],
+        },
+        {
+            name: 'No AFTER_INJECTION markers in easy build',
+            content: [AFTER_INJECTION, 0],
+        },
+    ];
+    const easyWithoutInjections: EasyExpectation[] = [
+        {
+            name: 'Normal log in easy build',
+            logs: {
+                'main.js': [normalLog, 1],
+            },
+            content: [`console.log("${normalLog}");`, 1],
+        },
+        ...noMarkers,
+    ];
+    const hardWithoutInjections: HardExpectation[] = [
+        {
+            name: 'Normal log in hard build',
+            logs: {
+                'app1.js': [normalLog, 1],
+                'app2.js': [normalLog, 0],
+            },
+            // Using only normalLog here, as imports and function names (console.log, chalk)
+            // are probably re-written and mangled by the bundlers.
+            content: [normalLog, 1],
+        },
+        ...noMarkers,
+    ];
+    const easyWithInjections: EasyExpectation[] = [
+        // We have the same expectation on the normalLog which is not due to injections.
+        easyWithoutInjections[0],
+        {
+            name: '[middle] code injection with imports in easy build',
+            logs: {
+                'main.js': [specialLog, 1],
+            },
+            // Using only 'specialLog' here, as imports and function names (console.log, chalk)
+            // are probably re-written and mangled by the bundlers.
+            content: [specialLog, 1],
+        },
+    ];
+    const hardWithInjections: HardExpectation[] = [
+        // We have the same expectation on the normalLog which is not due to injections.
+        hardWithoutInjections[0],
+        {
+            name: '[middle] code injection with imports in hard build',
+            logs: {
+                'app1.js': [specialLog, 1],
+                'app2.js': [specialLog, 1],
+            },
+            // Using only 'specialLog' here, as imports and function names (console.log, chalk)
+            // are probably re-written and mangled by the bundlers.
+            // Also, we don't know exactly how each bundler will concatenate the files.
+            // Since we have two entries here, we can expect the content
+            // to be repeated at least once and at most twice.
+            content: [specialLog, [1, 2]],
+        },
+    ];
+
+    const toInjectItems: ToInjectItem[] = [
+        // Add a special case of import to confirm this is working as expected in the middle of the code.
+        {
+            type: 'code',
+            value: `const chalk = require('chalk');\nconsole.log(chalk.bold.red('${specialLog}'));\n`,
+            position: InjectPosition.MIDDLE,
+        },
+    ];
+
+    // Build expectations and mock injections.
+    for (const type of Object.values(ContentType)) {
+        const injectType = type === ContentType.CODE ? 'code' : 'file';
+        for (const position of Object.values(Position)) {
+            const positionType =
+                position === Position.BEFORE
+                    ? InjectPosition.BEFORE
+                    : position === Position.MIDDLE
+                      ? InjectPosition.MIDDLE
+                      : InjectPosition.AFTER;
+
+            const injectionLog = getLog(type, position);
+            const injectionContent = getContent(type, position);
+            const injection: ToInjectItem = {
+                type: injectType,
+                value: injectionContent,
+                position: positionType,
+            };
+
+            // Fill in the expectations for each type of test.
+            hardWithInjections.push({
+                name: `[${position}] ${type} injection in hard build`,
+                logs: {
+                    'app1.js': [injectionLog, 1],
+                    'app2.js': [injectionLog, 1],
+                },
+                content: [injectionContent, [1, 2]],
+            });
+
+            easyWithInjections.push({
+                name: `[${position}] ${type} injection in easy build`,
+                logs: {
+                    'main.js': [injectionLog, 1],
+                },
+                content: [injectionContent, 1],
+            });
+
+            if (type === ContentType.DISTANT) {
+                injection.value = `${DOMAIN}${getFileUrl(position)}`;
+            } else if (type === ContentType.LOCAL) {
+                injection.value = `.${getFileUrl(position)}`;
+            }
+
+            toInjectItems.push(injection);
+        }
+    }
+
+    // Create a custom plugin to inject the files/codes into the build, store some states and tweak some output.
+    const getPlugins =
+        (
+            injections: ToInjectItem[] = [],
+            buildStates: Partial<Record<BundlerFullName, BuildState>>,
+        ): Options['customPlugins'] =>
+        (opts, context) => {
+            for (const injection of injections) {
+                context.inject(injection);
+            }
+
+            return [
+                {
+                    name: 'get-outdirs',
+                    writeBundle() {
+                        // Store the seeded outdir to inspect the produced files.
+                        const buildState: BuildState = buildStates[context.bundler.fullName] || {};
+                        buildState.outdir = context.bundler.outDir;
+                        buildStates[context.bundler.fullName] = buildState;
+
+                        // Add a package.json file to the esm builds.
+                        if (['esbuild'].includes(context.bundler.fullName)) {
+                            writeFileSync(
+                                path.resolve(context.bundler.outDir, 'package.json'),
+                                '{ "type": "module" }',
+                            );
+                        }
+                    },
+                },
+                ...debugFilesPlugins(context),
+            ];
+        };
+
+    // Define our tests.
+    const tests: {
+        name: string;
+        overrides: Parameters<typeof runBundlers>[1];
+        positions: Position[];
+        injections: [ToInjectItem[], number];
+        expectations: (EasyExpectation | HardExpectation)[];
+    }[] = [
+        {
+            name: 'Easy build without injections',
+            overrides: (workingDir: string) => getNodeSafeBuildOverrides(workingDir),
+            positions: [],
+            injections: [[], 0],
+            expectations: easyWithoutInjections,
+        },
+        {
+            name: 'Hard build without injections',
+            overrides: (workingDir: string) =>
+                getNodeSafeBuildOverrides(workingDir, getComplexBuildOverrides()),
+            positions: [],
+            injections: [[], 0],
+            expectations: hardWithoutInjections,
+        },
+        {
+            name: 'Easy build with injections',
+            overrides: (workingDir: string) => getNodeSafeBuildOverrides(workingDir),
+            positions: Object.values(Position),
+            injections: [toInjectItems, 10],
+            expectations: easyWithInjections,
+        },
+        {
+            name: 'Hard build with injections',
+            overrides: (workingDir: string) =>
+                getNodeSafeBuildOverrides(workingDir, getComplexBuildOverrides()),
+            positions: Object.values(Position),
+            injections: [toInjectItems, 10],
+            expectations: hardWithInjections,
+        },
+    ];
+
+    beforeAll(() => {
+        // Prepare mock files.
+        for (const position of Object.values(Position)) {
+            // NOTE: These files should already exist and have the correct content.
+            // It is just to confirm we keep the same content.
+            // We can't use memfs because bundlers, which read the files, runs within "jest.isolateModulesAsync"
+            // and don't have access to the same memfs' file system.
+            const fileContent = `${header(licenses.mit.name)}\n${getContent(ContentType.LOCAL, position)}`;
+            outputFileSync(`./src/_jest/fixtures${getFileUrl(position)}`, fileContent);
+        }
+    });
+
+    describe.each(tests)('$name', ({ overrides, positions, injections, expectations }) => {
+        let nockScope: nock.Scope;
+        let cleanup: CleanupFn;
+        let buildStates: Partial<Record<BundlerFullName, BuildState>> = {};
+
+        beforeAll(async () => {
+            nockScope = nock(DOMAIN);
+
+            // Prepare mock routes.
+            for (const position of positions) {
+                // Add mock route to file.
+                nockScope
+                    .get(getFileUrl(position))
+                    .times(BUNDLERS.length)
+                    .reply(200, getContent(ContentType.DISTANT, position));
+            }
+
+            cleanup = await runBundlers(
+                { customPlugins: getPlugins(injections[0], buildStates) },
+                overrides,
+            );
+
+            // Execute the builds and store some state.
+            const proms: Promise<void>[] = [];
+            for (const bundler of BUNDLERS) {
+                const buildState = buildStates[bundler.name];
+                const outdir = buildState?.outdir;
+
+                // This will be caught in the tests for each bundler.
+                if (!outdir || !buildState) {
+                    continue;
+                }
+
+                const builtFiles = glob.sync(path.resolve(outdir, '*.{js,mjs}'));
+
+                // Only execute files we identified as entries.
+                const filesToRun: File[] = builtFiles
+                    .map((file) => path.basename(file) as File)
+                    .filter((basename) => FILES.includes(basename));
+
+                // Run the files through node to confirm they don't crash and assert their logs.
+                proms.push(
+                    ...filesToRun.map(async (file) => {
+                        const result = await execute('node', [path.resolve(outdir, file)]);
+                        buildState.logs = buildState.logs || {};
+                        buildState.logs[file] = result.stdout;
+                    }),
+                );
+
+                // Store the content of the built files to assert the injections.
+                buildState.content = builtFiles
+                    .map((file) => readFileSync(file, 'utf8'))
+                    .join('\n');
+            }
+
+            await Promise.all(proms);
+            // Webpack can be slow to build...
+        }, 100000);
+
+        afterAll(async () => {
+            buildStates = {};
+            nock.cleanAll();
+            await cleanup();
+        });
+
+        test('Should have the correct test environment.', () => {
+            expect(injections[0]).toHaveLength(injections[1]);
+
+            // We should have called everything we've mocked for.
+            expect(nockScope.isDone()).toBe(true);
+        });
+
+        describe.each(BUNDLERS)('$name | $version', ({ name }) => {
+            let buildState: BuildState;
+
+            test('Should have a buildState.', () => {
+                buildState = buildStates[name]!;
+                expect(buildState).toBeDefined();
+                expect(buildState.outdir).toEqual(expect.any(String));
+                expect(buildState.logs).toEqual(expect.any(Object));
+                expect(buildState.content).toEqual(expect.any(String));
+            });
+
+            describe.each(expectations)(
+                '$name',
+                ({ content: [expectedContent, contentOccurencies], logs }) => {
+                    test('Should have the expected content in the bundles.', () => {
+                        const content = buildState.content;
+                        const expectation =
+                            expectedContent instanceof RegExp
+                                ? expectedContent
+                                : new RegExp(escapeStringForRegExp(expectedContent));
+
+                        expect(content).toRepeatStringTimes(expectation, contentOccurencies);
+                    });
+
+                    if (!logs) {
+                        return;
+                    }
+
+                    test('Should have output the expected logs from execution.', () => {
+                        const logExpectations = Object.entries(logs);
+                        for (const [file, [expectedLog, logOccurencies]] of logExpectations) {
+                            const stateLogs = buildState.logs?.[file as File];
+                            const expectation =
+                                expectedLog instanceof RegExp
+                                    ? expectedLog
+                                    : new RegExp(escapeStringForRegExp(expectedLog));
+
+                            expect(stateLogs).toBeDefined();
+                            expect(stateLogs).toRepeatStringTimes(expectation, logOccurencies);
+                        }
+                    });
+                },
+            );
+        });
+    });
+});
diff --git a/packages/tests/src/plugins/telemetry/.eslintrc.js b/packages/tests/src/unit/plugins/telemetry/.eslintrc.js
similarity index 100%
rename from packages/tests/src/plugins/telemetry/.eslintrc.js
rename to packages/tests/src/unit/plugins/telemetry/.eslintrc.js
diff --git a/packages/tests/src/plugins/telemetry/common/aggregator.test.ts b/packages/tests/src/unit/plugins/telemetry/common/aggregator.test.ts
similarity index 87%
rename from packages/tests/src/plugins/telemetry/common/aggregator.test.ts
rename to packages/tests/src/unit/plugins/telemetry/common/aggregator.test.ts
index 027459d46..b713ceb45 100644
--- a/packages/tests/src/plugins/telemetry/common/aggregator.test.ts
+++ b/packages/tests/src/unit/plugins/telemetry/common/aggregator.test.ts
@@ -4,7 +4,7 @@
 
 import { addMetrics } from '@dd/telemetry-plugin/common/aggregator';
 import { getContextMock } from '@dd/tests/_jest/helpers/mocks';
-import { mockOptionsDD, mockReport } from '@dd/tests/plugins/telemetry/testHelpers';
+import { mockOptionsDD, mockReport } from '@dd/tests/unit/plugins/telemetry/testHelpers';
 
 describe('Telemetry Aggregator', () => {
     test('Should aggregate metrics without throwing.', () => {
diff --git a/packages/tests/src/plugins/telemetry/common/helpers.test.ts b/packages/tests/src/unit/plugins/telemetry/common/helpers.test.ts
similarity index 100%
rename from packages/tests/src/plugins/telemetry/common/helpers.test.ts
rename to packages/tests/src/unit/plugins/telemetry/common/helpers.test.ts
diff --git a/packages/tests/src/plugins/telemetry/common/output/files.test.ts b/packages/tests/src/unit/plugins/telemetry/common/output/files.test.ts
similarity index 97%
rename from packages/tests/src/plugins/telemetry/common/output/files.test.ts
rename to packages/tests/src/unit/plugins/telemetry/common/output/files.test.ts
index 7badb15a0..3a406a942 100644
--- a/packages/tests/src/plugins/telemetry/common/output/files.test.ts
+++ b/packages/tests/src/unit/plugins/telemetry/common/output/files.test.ts
@@ -5,7 +5,7 @@
 import { outputFiles } from '@dd/telemetry-plugin/common/output/files';
 import type { OutputOptions } from '@dd/telemetry-plugin/types';
 import { mockLogger } from '@dd/tests/_jest/helpers/mocks';
-import { mockReport } from '@dd/tests/plugins/telemetry/testHelpers';
+import { mockReport } from '@dd/tests/unit/plugins/telemetry/testHelpers';
 import { vol } from 'memfs';
 import path from 'path';
 
diff --git a/packages/tests/src/plugins/telemetry/esbuild-plugin/plugins.test.ts b/packages/tests/src/unit/plugins/telemetry/esbuild-plugin/plugins.test.ts
similarity index 94%
rename from packages/tests/src/plugins/telemetry/esbuild-plugin/plugins.test.ts
rename to packages/tests/src/unit/plugins/telemetry/esbuild-plugin/plugins.test.ts
index 0ddd95dc9..df16f3b1b 100644
--- a/packages/tests/src/plugins/telemetry/esbuild-plugin/plugins.test.ts
+++ b/packages/tests/src/unit/plugins/telemetry/esbuild-plugin/plugins.test.ts
@@ -3,7 +3,7 @@
 // Copyright 2019-Present Datadog, Inc.
 
 import { wrapPlugins, getResults } from '@dd/telemetry-plugin/esbuild-plugin/plugins';
-import { getMockBuild } from '@dd/tests/plugins/telemetry/testHelpers';
+import { getMockBuild } from '@dd/tests/unit/plugins/telemetry/testHelpers';
 import type { PluginBuild, Plugin } from 'esbuild';
 
 describe('Telemetry ESBuild Plugins', () => {
diff --git a/packages/tests/src/plugins/telemetry/index.test.ts b/packages/tests/src/unit/plugins/telemetry/index.test.ts
similarity index 87%
rename from packages/tests/src/plugins/telemetry/index.test.ts
rename to packages/tests/src/unit/plugins/telemetry/index.test.ts
index 51bd84b27..28b963d79 100644
--- a/packages/tests/src/plugins/telemetry/index.test.ts
+++ b/packages/tests/src/unit/plugins/telemetry/index.test.ts
@@ -7,12 +7,12 @@ import { addMetrics } from '@dd/telemetry-plugin/common/aggregator';
 import type { MetricToSend } from '@dd/telemetry-plugin/types';
 import {
     FAKE_URL,
-    debugFilesPlugins,
     filterOutParticularities,
     getComplexBuildOverrides,
 } from '@dd/tests/_jest/helpers/mocks';
 import { BUNDLERS, runBundlers } from '@dd/tests/_jest/helpers/runBundlers';
 import type { Bundler, CleanupFn } from '@dd/tests/_jest/helpers/types';
+import { debugFilesPlugins } from '@dd/tools/helpers';
 import nock from 'nock';
 
 // Used to intercept metrics.
@@ -114,41 +114,39 @@ describe('Telemetry Universal Plugin', () => {
 
         // We don't want to crash if there are no bundlers to test here.
         // Which can happen when using --bundlers.
-        if (expectations.length > 0) {
-            let cleanup: CleanupFn;
-
-            beforeAll(async () => {
-                const pluginConfig: Options = {
-                    telemetry: {
-                        enableTracing: true,
-                        endPoint: FAKE_URL,
-                        filters: [],
-                    },
-                    logLevel: 'warn',
-                    customPlugins: (options: Options, context: GlobalContext) =>
-                        debugFilesPlugins(context),
-                };
-                // This one is called at initialization, with the initial context.
-                addMetricsMocked.mockImplementation(getAddMetricsImplem(metrics));
-                cleanup = await runBundlers(
-                    pluginConfig,
-                    getComplexBuildOverrides(),
-                    activeBundlers,
-                );
-            });
+        if (!expectations.length) {
+            return;
+        }
 
-            afterAll(async () => {
-                await cleanup();
-            });
+        let cleanup: CleanupFn;
 
-            test.each(expectations)(
-                '$name - $version | Should get the related metrics',
-                ({ name, expectedMetrics }) => {
-                    const metricNames = metrics[name].map((metric) => metric.metric).sort();
-                    expect(metricNames).toEqual(expect.arrayContaining(expectedMetrics));
+        beforeAll(async () => {
+            const pluginConfig: Options = {
+                telemetry: {
+                    enableTracing: true,
+                    endPoint: FAKE_URL,
+                    filters: [],
                 },
-            );
-        }
+                logLevel: 'warn',
+                customPlugins: (options: Options, context: GlobalContext) =>
+                    debugFilesPlugins(context),
+            };
+            // This one is called at initialization, with the initial context.
+            addMetricsMocked.mockImplementation(getAddMetricsImplem(metrics));
+            cleanup = await runBundlers(pluginConfig, getComplexBuildOverrides(), activeBundlers);
+        });
+
+        afterAll(async () => {
+            await cleanup();
+        });
+
+        test.each(expectations)(
+            '$name - $version | Should get the related metrics',
+            ({ name, expectedMetrics }) => {
+                const metricNames = metrics[name].map((metric) => metric.metric).sort();
+                expect(metricNames).toEqual(expect.arrayContaining(expectedMetrics));
+            },
+        );
     });
 
     describe('Without enableTracing', () => {
@@ -327,22 +325,10 @@ describe('Telemetry Universal Plugin', () => {
 
             // [name, entryNames, size, dependencies, dependents];
             const modulesExpectations: [string, string[], number, number, number][] = [
-                [
-                    'src/_jest/fixtures/project/workspaces/app/workspaceFile0.js',
-                    ['app1', 'app2'],
-                    30042,
-                    0,
-                    2,
-                ],
-                [
-                    'src/_jest/fixtures/project/workspaces/app/workspaceFile1.js',
-                    ['app1', 'app2'],
-                    4600,
-                    1,
-                    2,
-                ],
-                ['src/_jest/fixtures/project/src/srcFile1.js', ['app2'], 2237, 2, 1],
-                ['src/_jest/fixtures/project/src/srcFile0.js', ['app1', 'app2'], 13248, 1, 3],
+                ['hard_project/workspaces/app/workspaceFile0.js', ['app1', 'app2'], 30042, 0, 2],
+                ['hard_project/workspaces/app/workspaceFile1.js', ['app1', 'app2'], 4600, 1, 2],
+                ['hard_project/src/srcFile1.js', ['app2'], 2237, 2, 1],
+                ['hard_project/src/srcFile0.js', ['app1', 'app2'], 13248, 1, 3],
                 ['escape-string-regexp/index.js', ['app1'], 226, 0, 1],
                 ['color-name/index.js', ['app1'], 4617, 0, 1],
                 ['color-convert/conversions.js', ['app1'], 16850, 1, 2],
@@ -353,8 +339,8 @@ describe('Telemetry Universal Plugin', () => {
                 ['chalk/templates.js', ['app1'], 3133, 0, 1],
                 // Somehow rollup and vite are not reporting the same size.
                 ['chalk/index.js', ['app1'], expect.toBeWithinRange(6437, 6439), 4, 1],
-                ['src/_jest/fixtures/project/main1.js', ['app1'], 462, 3, 0],
-                ['src/_jest/fixtures/project/main2.js', ['app2'], 337, 2, 0],
+                ['hard_project/main1.js', ['app1'], 462, 3, 0],
+                ['hard_project/main2.js', ['app2'], 337, 2, 0],
             ];
 
             describe.each(modulesExpectations)(
diff --git a/packages/tests/src/plugins/telemetry/testHelpers.ts b/packages/tests/src/unit/plugins/telemetry/testHelpers.ts
similarity index 100%
rename from packages/tests/src/plugins/telemetry/testHelpers.ts
rename to packages/tests/src/unit/plugins/telemetry/testHelpers.ts
diff --git a/packages/tests/src/tools/src/commands/create-plugin/ask.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/ask.test.ts
similarity index 100%
rename from packages/tests/src/tools/src/commands/create-plugin/ask.test.ts
rename to packages/tests/src/unit/tools/src/commands/create-plugin/ask.test.ts
diff --git a/packages/tests/src/tools/src/commands/create-plugin/hooks.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/hooks.test.ts
similarity index 100%
rename from packages/tests/src/tools/src/commands/create-plugin/hooks.test.ts
rename to packages/tests/src/unit/tools/src/commands/create-plugin/hooks.test.ts
diff --git a/packages/tests/src/tools/src/commands/create-plugin/index.test.ts b/packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts
similarity index 100%
rename from packages/tests/src/tools/src/commands/create-plugin/index.test.ts
rename to packages/tests/src/unit/tools/src/commands/create-plugin/index.test.ts
diff --git a/packages/tests/src/tools/src/commands/integrity/files.test.ts b/packages/tests/src/unit/tools/src/commands/integrity/files.test.ts
similarity index 100%
rename from packages/tests/src/tools/src/commands/integrity/files.test.ts
rename to packages/tests/src/unit/tools/src/commands/integrity/files.test.ts
diff --git a/packages/tests/src/tools/src/helpers.test.ts b/packages/tests/src/unit/tools/src/helpers.test.ts
similarity index 100%
rename from packages/tests/src/tools/src/helpers.test.ts
rename to packages/tests/src/unit/tools/src/helpers.test.ts
diff --git a/packages/tests/src/tools/src/rollupConfig.test.ts b/packages/tests/src/unit/tools/src/rollupConfig.test.ts
similarity index 56%
rename from packages/tests/src/tools/src/rollupConfig.test.ts
rename to packages/tests/src/unit/tools/src/rollupConfig.test.ts
index f7fffa49a..9fd346efb 100644
--- a/packages/tests/src/tools/src/rollupConfig.test.ts
+++ b/packages/tests/src/unit/tools/src/rollupConfig.test.ts
@@ -6,7 +6,8 @@ import { datadogEsbuildPlugin } from '@datadog/esbuild-plugin';
 import { datadogRollupPlugin } from '@datadog/rollup-plugin';
 import { datadogRspackPlugin } from '@datadog/rspack-plugin';
 import { datadogVitePlugin } from '@datadog/vite-plugin';
-import { formatDuration, rm } from '@dd/core/helpers';
+import { SUPPORTED_BUNDLERS } from '@dd/core/constants';
+import { formatDuration, getUniqueId, rm } from '@dd/core/helpers';
 import type { BundlerFullName, Options } from '@dd/core/types';
 import {
     getEsbuildOptions,
@@ -14,11 +15,12 @@ import {
     getWebpack4Options,
     getWebpack5Options,
 } from '@dd/tests/_jest/helpers/configBundlers';
-import { BUNDLER_VERSIONS, NO_CLEANUP } from '@dd/tests/_jest/helpers/constants';
+import { BUNDLER_VERSIONS } from '@dd/tests/_jest/helpers/constants';
+import { getOutDir, prepareWorkingDir } from '@dd/tests/_jest/helpers/env';
+import { getWebpackPlugin } from '@dd/tests/_jest/helpers/getWebpackPlugin';
 import {
     API_PATH,
     FAKE_URL,
-    defaultDestination,
     defaultEntries,
     getComplexBuildOverrides,
     getFullPluginConfig,
@@ -32,11 +34,11 @@ import {
     runWebpack5,
 } from '@dd/tests/_jest/helpers/runBundlers';
 import type { CleanupFn } from '@dd/tests/_jest/helpers/types';
-import { getWebpack4Entries, getWebpackPlugin } from '@dd/tests/_jest/helpers/xpackConfigs';
 import { ROOT } from '@dd/tools/constants';
-import { bgYellow, execute, green } from '@dd/tools/helpers';
+import { bgGreen, bgYellow, execute, green } from '@dd/tools/helpers';
 import type { BuildOptions } from 'esbuild';
 import fs from 'fs';
+import { glob } from 'glob';
 import nock from 'nock';
 import path from 'path';
 
@@ -58,8 +60,9 @@ jest.mock('@datadog/webpack-plugin', () => ({
 }));
 
 // Mock the plugin configuration of webpack to actually use the bundled plugin.
-jest.mock('@dd/tests/_jest/helpers/xpackConfigs', () => {
-    const actual = jest.requireActual('@dd/tests/_jest/helpers/xpackConfigs');
+// And pass the correct bundler to it (webpack4 or webpack5).
+jest.mock('@dd/tests/_jest/helpers/getWebpackPlugin', () => {
+    const actual = jest.requireActual('@dd/tests/_jest/helpers/getWebpackPlugin');
     return {
         ...actual,
         getWebpackPlugin: jest.fn(),
@@ -72,12 +75,18 @@ const datadogRspackPluginMock = jest.mocked(datadogRspackPlugin);
 const datadogVitePluginMock = jest.mocked(datadogVitePlugin);
 const getWebpackPluginMock = jest.mocked(getWebpackPlugin);
 
+const getPackagePath = (bundlerName: string) => {
+    // Cover for names that have a version in it, eg: webpack5, webpack4.
+    const cleanBundlerName = SUPPORTED_BUNDLERS.find((name) => bundlerName.startsWith(name));
+    if (!cleanBundlerName) {
+        throw new Error(`Bundler not supported: "${bundlerName}"`);
+    }
+    return path.resolve(ROOT, `packages/published/${cleanBundlerName}-plugin/dist/src`);
+};
+
 // Ensure our packages have been built not too long ago.
 const getPackageDestination = (bundlerName: string) => {
-    const packageDestination = path.resolve(
-        ROOT,
-        `packages/published/${bundlerName}-plugin/dist/src`,
-    );
+    const packageDestination = getPackagePath(bundlerName);
 
     // If we don't need this bundler, no need to check for its bundle.
     if (BUNDLERS.find((bundler) => bundler.name.startsWith(bundlerName)) === undefined) {
@@ -89,6 +98,17 @@ const getPackageDestination = (bundlerName: string) => {
         const stats = fs.statSync(packageDestination);
         const lastUpdateDuration = Math.ceil((new Date().getTime() - stats.mtimeMs) / 1000) * 1000;
 
+        // If we're in the CI it means we're using cached files.
+        if (process.env.CI) {
+            console.log(
+                bgGreen(
+                    ` [CACHED] ${bundlerName}-plugin was built ${formatDuration(lastUpdateDuration)} ago.\n`,
+                ),
+            );
+            // We don't want to block/alert on builds in CI.
+            return packageDestination;
+        }
+
         // If last build was more than 10 minutes ago, warn the user.
         if (lastUpdateDuration > 1000 * 60 * 10) {
             console.log(
@@ -115,10 +135,25 @@ const getPackageDestination = (bundlerName: string) => {
     return packageDestination;
 };
 
+const getBuiltFiles = () => {
+    const pkgs = glob.sync('packages/plugins/**/package.json', { cwd: ROOT });
+    const builtFiles = [];
+
+    for (const pkg of pkgs) {
+        const content = require(path.resolve(ROOT, pkg));
+        if (!content.toBuild) {
+            continue;
+        }
+
+        builtFiles.push(...Object.keys(content.toBuild).map((f) => `${f}.js`));
+    }
+
+    return builtFiles;
+};
+
 describe('Bundling', () => {
     let bundlerVersions: Partial<Record<BundlerFullName, string>> = {};
     let processErrors: string[] = [];
-    const seededFolders: string[] = [];
     const pluginConfig = getFullPluginConfig({
         logLevel: 'error',
         customPlugins: (opts, context) => [
@@ -143,12 +178,12 @@ describe('Bundling', () => {
         // Duplicate the webpack plugin to have one with webpack 4 and one with webpack 5.
         const webpack5Plugin = getPackageDestination('webpack');
         const webpack4Plugin = path.resolve(webpack5Plugin, 'index4.js');
-        // Create a new file that will use webpack4.
+        // Create a new file that will use webpack4 instead of webpack.
         fs.writeFileSync(
             webpack4Plugin,
             fs
                 .readFileSync(path.resolve(webpack5Plugin, 'index.js'), { encoding: 'utf-8' })
-                .replace("require('webpack')", "require('webpack4')"),
+                .replace(/require\(('|")webpack("|')\)/g, "require('webpack4')"),
         );
 
         // Make the mocks target the built packages.
@@ -182,12 +217,17 @@ describe('Bundling', () => {
             .reply(200, {});
 
         // Intercept Node errors. (Especially DeprecationWarnings in the case of Webpack5).
-        const actualConsoleError = console.error;
+        const actualConsoleError = jest.requireActual('console').error;
         // Filter out the errors we expect.
         const ignoredErrors = [
+            // Used for Jest runtime in "yarn test:unit".
             'ExperimentalWarning: VM Modules',
+            // Used in our sourcemaps sender, to build a stream of our zipped sourcemaps.
             'ExperimentalWarning: buffer.File',
+            // Used in Unplugin's xpack loaders.
+            'fs.rmdir(path, { recursive: true })',
         ];
+
         // NOTE: this will trigger only once per session, per error.
         jest.spyOn(console, 'error').mockImplementation((err) => {
             if (!ignoredErrors.some((e) => err.includes(e))) {
@@ -205,97 +245,94 @@ describe('Bundling', () => {
 
     afterAll(async () => {
         nock.cleanAll();
-        if (NO_CLEANUP) {
-            return;
-        }
-        console.log('[rollupConfig | Bundling] Cleaning up seeded folders.\n', seededFolders);
-        await Promise.all(seededFolders.map((folder) => rm(folder)));
     });
 
     const nameSize = Math.max(...BUNDLERS.map((bundler) => bundler.name.length)) + 1;
-    const TIMESTAMP = Date.now();
-
-    describe.each(BUNDLERS)('Bundler: $name', (bundler) => {
-        test('Should not throw on a easy project.', async () => {
-            const projectName = 'easy';
-            const timeId = `[ ${green(bundler.name.padEnd(nameSize))}] ${green(projectName)} run`;
-            console.time(timeId);
-
-            const SEED = `${TIMESTAMP}-${projectName}-${jest.getSeed()}`;
-            const rootDir = path.resolve(defaultDestination, SEED);
-            const outdir = path.resolve(rootDir, bundler.name);
-            seededFolders.push(rootDir);
-            const bundlerConfig = bundler.config(
-                SEED,
-                pluginConfig,
-                getNodeSafeBuildOverrides()[bundler.name],
-            );
-
-            if (!bundlerConfig) {
-                throw new Error(`Missing bundlerConfig for ${bundler.name}.`);
-            }
-
-            const bundlerConfigOverrides =
-                bundler.name === 'vite' ? bundlerConfig.build.rollupOptions : bundlerConfig;
-            const { errors } = await bundler.run(SEED, pluginConfig, bundlerConfigOverrides);
-            expect(errors).toHaveLength(0);
 
-            // Test the actual bundled file too.
-            await expect(execute('node', [path.resolve(outdir, 'main.js')])).resolves.not.toThrow();
-
-            // It should use the correct version of the bundler.
-            // This is to ensure our test is running in the right conditions.
-            expect(bundlerVersions[bundler.name]).toBe(BUNDLER_VERSIONS[bundler.name]);
-
-            // It should not have printed any error.
-            expect(processErrors).toHaveLength(0);
-
-            console.timeEnd(timeId);
-
-            // Adding some timeout because webpack is SLOW.
-        }, 10000);
-
-        test('Should not throw on a hard project.', async () => {
-            const projectName = 'hard';
-            const timeId = `[ ${green(bundler.name.padEnd(nameSize))}] ${green(projectName)} run`;
-            console.time(timeId);
+    describe.each(
+        // Only do bundlers that are requested to be tested.
+        SUPPORTED_BUNDLERS.filter((bundlerName: string) =>
+            BUNDLERS.find((bundler) => bundler.name.startsWith(bundlerName)),
+        ),
+    )('Bundler: %s', (bundlerName) => {
+        test(`Should add the correct files to @datadog/${bundlerName}-plugin.`, () => {
+            const builtFiles = getBuiltFiles();
+            const expectedFiles = [
+                'index.d.ts',
+                'index.js',
+                'index.js.map',
+                'index.mjs',
+                'index.mjs.map',
+                ...builtFiles,
+            ].sort();
+            const existingFiles = fs.readdirSync(getPackagePath(bundlerName)).sort();
+            const exceptions = [
+                // We are adding this file ourselves in the test to test both webpack4 and webpack5.
+                'index4.js',
+            ];
+            expect(existingFiles.filter((f) => !exceptions.includes(f))).toEqual(expectedFiles);
+        });
+    });
 
-            const SEED = `${TIMESTAMP}-${projectName}-${jest.getSeed()}`;
-            const rootDir = path.resolve(defaultDestination, SEED);
-            const outdir = path.resolve(rootDir, bundler.name);
-            seededFolders.push(rootDir);
-            const bundlerConfig = bundler.config(
-                SEED,
-                pluginConfig,
-                getNodeSafeBuildOverrides(getComplexBuildOverrides())[bundler.name],
-            );
+    describe.each(BUNDLERS)('Bundler: $name', (bundler) => {
+        test.each<{ projectName: string; filesToRun: string[] }>([
+            { projectName: 'easy', filesToRun: ['main.js'] },
+            { projectName: 'hard', filesToRun: ['app1.js', 'app2.js'] },
+        ])(
+            'Should not throw on $projectName project.',
+            async ({ projectName, filesToRun }) => {
+                const timeId = `[ ${green(bundler.name.padEnd(nameSize))}] ${green(projectName)} run`;
+                console.time(timeId);
+
+                const SEED = `${jest.getSeed()}.${projectName}.${getUniqueId()}`;
+                const rootDir = await prepareWorkingDir(SEED);
+                const overrides = getNodeSafeBuildOverrides(
+                    rootDir,
+                    projectName === 'hard' ? getComplexBuildOverrides() : {},
+                );
+                const outdir = getOutDir(rootDir, bundler.name);
+                const bundlerConfig = bundler.config(
+                    rootDir,
+                    pluginConfig,
+                    overrides[bundler.name],
+                );
+
+                if (!bundlerConfig) {
+                    throw new Error(`Missing bundlerConfig for ${bundler.name}.`);
+                }
 
-            if (!bundlerConfig) {
-                throw new Error(`Missing bundlerConfig for ${bundler.name}.`);
-            }
+                // Our vite run function has a slightly different signature due to how it sets up its bundling.
+                const bundlerConfigOverrides =
+                    bundler.name === 'vite' ? bundlerConfig.build.rollupOptions : bundlerConfig;
 
-            // Vite only overrides its options.build.rollupOptions.
-            const bundlerConfigOverrides =
-                bundler.name === 'vite' ? bundlerConfig.build.rollupOptions : bundlerConfig;
+                const { errors } = await bundler.run(rootDir, pluginConfig, bundlerConfigOverrides);
+                expect(errors).toHaveLength(0);
 
-            const { errors } = await bundler.run(SEED, pluginConfig, bundlerConfigOverrides);
-            expect(errors).toHaveLength(0);
+                // Test the actual bundled files too.
+                await Promise.all(
+                    filesToRun
+                        .map((f) => path.resolve(outdir, f))
+                        .map((file) => expect(execute('node', [file])).resolves.not.toThrow()),
+                );
 
-            // Test the actual bundled file too.
-            await expect(execute('node', [path.resolve(outdir, 'app1.js')])).resolves.not.toThrow();
-            await expect(execute('node', [path.resolve(outdir, 'app2.js')])).resolves.not.toThrow();
+                // It should use the correct version of the bundler.
+                // This is to ensure our test is running in the right conditions.
+                expect(bundlerVersions[bundler.name]).toBe(BUNDLER_VERSIONS[bundler.name]);
 
-            // It should use the correct version of the bundler.
-            // This is to ensure our test is running in the right conditions.
-            expect(bundlerVersions[bundler.name]).toBe(BUNDLER_VERSIONS[bundler.name]);
+                // It should not have printed any error.
+                expect(processErrors).toHaveLength(0);
 
-            // It should not have printed any error.
-            expect(processErrors).toHaveLength(0);
+                // Clean working dir.
+                if (!process.env.NO_CLEANUP) {
+                    await rm(rootDir);
+                }
 
-            console.timeEnd(timeId);
+                console.timeEnd(timeId);
 
-            // Adding some timeout because webpack is SLOW.
-        }, 10000);
+                // Adding some timeout because webpack is SLOW.
+            },
+            10000,
+        );
     });
 
     test('Should not throw on a weird project.', async () => {
@@ -303,21 +340,20 @@ describe('Bundling', () => {
         const timeId = `[ ${green('esbuild + webpack + rspack')}] ${green(projectName)} run`;
         console.time(timeId);
 
-        const SEED = `${TIMESTAMP}-${projectName}-${jest.getSeed()}`;
-        const rootDir = path.resolve(defaultDestination, SEED);
-        const outdir = path.resolve(rootDir, projectName);
-        seededFolders.push(rootDir);
-        const configs = getNodeSafeBuildOverrides(getComplexBuildOverrides());
+        const SEED = `${jest.getSeed()}.${getUniqueId()}`;
+        const rootDir = await prepareWorkingDir(SEED);
 
-        // Build esbuild somewhere temporary first.
-        const esbuildOutdir = path.resolve(outdir, './temp');
+        const overrides = getNodeSafeBuildOverrides(rootDir, getComplexBuildOverrides());
+        const esbuildOverrides = overrides.esbuild;
 
         // Configure bundlers.
-        const baseEsbuildConfig = getEsbuildOptions(SEED, {}, configs.esbuild);
+        const baseEsbuildConfig = getEsbuildOptions(rootDir, {}, esbuildOverrides);
+        const esbuildOutdir = baseEsbuildConfig.outdir!;
+
         const esbuildConfig1: BuildOptions = {
             ...baseEsbuildConfig,
-            outdir: esbuildOutdir,
-            entryPoints: { app1: defaultEntries.app1 },
+            // Only one entry, we'll build the second one in a parallel build.
+            entryPoints: { app1: path.resolve(rootDir, defaultEntries.app1) },
             plugins: [
                 ...(baseEsbuildConfig.plugins || []),
                 // Add a custom loader that will build a new file using the parent configuration.
@@ -325,9 +361,9 @@ describe('Bundling', () => {
                     name: 'custom-build-loader',
                     setup(build) {
                         build.onLoad({ filter: /.*\/main1\.js/ }, async ({ path: filepath }) => {
-                            const outfile = path.resolve(esbuildOutdir, 'app1.2.js');
+                            const outfile = path.resolve(build.initialOptions.outdir!, 'app1.2.js');
                             await runEsbuild(
-                                SEED,
+                                rootDir,
                                 {},
                                 {
                                     ...build.initialOptions,
@@ -349,36 +385,30 @@ describe('Bundling', () => {
 
         // Add a second parallel build.
         const esbuildConfig2: BuildOptions = {
-            ...baseEsbuildConfig,
-            outdir: esbuildOutdir,
-            entryPoints: { app2: defaultEntries.app2 },
+            ...getEsbuildOptions(rootDir, {}, overrides.esbuild),
+            entryPoints: { app2: path.resolve(rootDir, defaultEntries.app2) },
         };
 
         // Webpack triggers some deprecations warnings only when we have multi-entry entries.
-        const webpackEntries = {
-            app1: [
-                path.resolve(esbuildOutdir, 'app1.js'),
-                '@dd/tests/_jest/fixtures/project/empty.js',
-            ],
-            app2: [
-                path.resolve(esbuildOutdir, 'app2.js'),
-                '@dd/tests/_jest/fixtures/project/empty.js',
-            ],
-        };
+        // Use a function to generate a new object each time.
+        const xpackEntries = () => ({
+            app1: [path.resolve(esbuildOutdir, 'app1.js'), path.resolve(rootDir, './empty.js')],
+            app2: [path.resolve(esbuildOutdir, 'app2.js'), path.resolve(rootDir, './empty.js')],
+        });
 
         const rspackConfig = {
-            ...getRspackOptions(SEED, {}, configs.rspack),
-            entry: webpackEntries,
+            ...getRspackOptions(rootDir, {}, overrides.rspack),
+            entry: xpackEntries(),
         };
 
         const webpack5Config = {
-            ...getWebpack5Options(SEED, {}, configs.webpack5),
-            entry: webpackEntries,
+            ...getWebpack5Options(rootDir, {}, overrides.webpack5),
+            entry: xpackEntries(),
         };
 
         const webpack4Config = {
-            ...getWebpack4Options(SEED, {}, configs.webpack4),
-            entry: getWebpack4Entries(webpackEntries),
+            ...getWebpack4Options(rootDir, {}, overrides.webpack4),
+            entry: xpackEntries(),
         };
 
         // Build the sequence.
@@ -386,14 +416,14 @@ describe('Bundling', () => {
         const sequence: (() => Promise<SequenceReturn | SequenceReturn[]>)[] = [
             () =>
                 Promise.all([
-                    runEsbuild(SEED, pluginConfig, esbuildConfig1),
-                    runEsbuild(SEED, pluginConfig, esbuildConfig2),
+                    runEsbuild(rootDir, pluginConfig, esbuildConfig1),
+                    runEsbuild(rootDir, pluginConfig, esbuildConfig2),
                 ]),
             () =>
                 Promise.all([
-                    runWebpack5(SEED, pluginConfig, webpack5Config),
-                    runWebpack4(SEED, pluginConfig, webpack4Config),
-                    runRspack(SEED, pluginConfig, rspackConfig),
+                    runWebpack5(rootDir, pluginConfig, webpack5Config),
+                    runWebpack4(rootDir, pluginConfig, webpack4Config),
+                    runRspack(rootDir, pluginConfig, rspackConfig),
                 ]),
         ];
 
@@ -415,6 +445,11 @@ describe('Bundling', () => {
         // It should not have printed any error.
         expect(processErrors).toHaveLength(0);
 
+        // Clean working dir.
+        if (!process.env.NO_CLEANUP) {
+            await rm(rootDir);
+        }
+
         console.timeEnd(timeId);
     });
 });
diff --git a/packages/tools/package.json b/packages/tools/package.json
index b81c6c57f..bb0697d86 100644
--- a/packages/tools/package.json
+++ b/packages/tools/package.json
@@ -13,28 +13,49 @@
     "packageManager": "yarn@4.0.2",
     "exports": {
         "./rollupConfig.mjs": "./src/rollupConfig.mjs",
+        "./commands/oss/templates": "./src/commands/oss/templates.ts",
+        "./bundlers": "./src/bundlers.ts",
         "./*": "./src/*.ts"
     },
     "scripts": {
         "cli": "ts-node -T --project ./tsconfig.json ./src/index.ts"
     },
     "devDependencies": {
+        "@rollup/plugin-esm-shim": "0.1.7",
         "@types/chalk": "2.2.0",
         "@types/glob": "7.1.4",
+        "@types/lodash.template": "^4",
         "@types/node": "^18",
+        "@types/webpack4": "npm:@types/webpack@4.41.38",
+        "lodash.template": "4.5.0",
         "ts-node": "10.9.2",
         "typescript": "5.4.3"
     },
     "dependencies": {
+        "@datadog/esbuild-plugin": "workspace:*",
+        "@datadog/rollup-plugin": "workspace:*",
+        "@datadog/rspack-plugin": "workspace:*",
+        "@datadog/vite-plugin": "workspace:*",
+        "@datadog/webpack-plugin": "workspace:*",
         "@dd/assets": "workspace:*",
         "@dd/core": "workspace:*",
+        "@dd/error-tracking-plugin": "workspace:*",
+        "@dd/telemetry-plugin": "workspace:*",
         "@inquirer/checkbox": "2.3.3",
         "@inquirer/input": "2.1.7",
         "@inquirer/select": "2.3.3",
+        "@rollup/plugin-commonjs": "28.0.1",
+        "@rollup/plugin-node-resolve": "15.3.0",
+        "@rspack/core": "1.1.2",
         "chalk": "2.3.1",
         "clipanion": "4.0.0-rc.3",
+        "esbuild": "0.24.0",
         "glob": "7.1.6",
         "outdent": "0.8.0",
-        "typanion": "3.14.0"
+        "typanion": "3.14.0",
+        "vite": "5.4.10",
+        "webpack": "5.92.1",
+        "webpack4": "npm:webpack@4.47.0",
+        "webpack5": "npm:webpack@5.92.1"
     }
 }
diff --git a/packages/tools/src/bundlers.ts b/packages/tools/src/bundlers.ts
new file mode 100644
index 000000000..ecdf356fe
--- /dev/null
+++ b/packages/tools/src/bundlers.ts
@@ -0,0 +1,306 @@
+// 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 { BundlerFullName } from '@dd/core/types';
+import commonjs from '@rollup/plugin-commonjs';
+import { nodeResolve } from '@rollup/plugin-node-resolve';
+import type {
+    RspackOptions,
+    Stats as RspackStats,
+    StatsCompilation as RspackStatsCompilation,
+} from '@rspack/core';
+import type { BuildOptions, BuildResult } from 'esbuild';
+import path from 'path';
+import type { RollupOptions, RollupOutput } from 'rollup';
+import type { InlineConfig } from 'vite';
+import type { Configuration as Configuration4, Stats as Stats4 } from 'webpack4';
+import type {
+    Configuration as Configuration5,
+    Stats as Stats5,
+    StatsCompilation as StatsCompilation5,
+} from 'webpack5';
+
+export type BundlerOptions =
+    | RspackOptions
+    | Configuration4
+    | Configuration5
+    | BuildOptions
+    | RollupOptions
+    | InlineConfig;
+export type BundlerConfig = {
+    workingDir: string;
+    outDir: string;
+    entry: { [name: string]: string };
+    plugins?: any[];
+};
+export type BundlerConfigFunction = (config: BundlerConfig) => BundlerOptions;
+export type BundlerRunFunction = (
+    bundlerConfig: any,
+) => Promise<{ errors: string[]; result?: any }>;
+
+const xpackCallback = (
+    err: Error | null,
+    stats: Stats4 | Stats5 | RspackStats | undefined,
+    resolve: (value: unknown) => void,
+    reject: (reason?: any) => void,
+    delay: number = 0,
+) => {
+    if (err) {
+        reject(err);
+        return;
+    }
+
+    if (!stats) {
+        reject('No stats returned.');
+        return;
+    }
+
+    const { errors, warnings } = stats.compilation;
+    if (errors?.length) {
+        reject(errors[0]);
+        return;
+    }
+
+    if (warnings?.length) {
+        console.warn(warnings.join('\n'));
+    }
+
+    // Delay the resolve to give time to the bundler to finish writing the files.
+    // Webpack4 in particular is impacted by this and otherwise triggers a
+    // "Jest did not exit one second after the test run has completed." warning.
+    // TODO: Investigate this need for a delay after webpack 4's build.
+    setTimeout(() => {
+        resolve(stats);
+    }, delay);
+};
+
+export const buildWithRspack: BundlerRunFunction = async (bundlerConfig: RspackOptions) => {
+    const { rspack } = await import('@rspack/core');
+    const errors = [];
+    let result: RspackStatsCompilation | undefined;
+
+    try {
+        await new Promise((resolve, reject) => {
+            rspack(bundlerConfig, (err, stats) => {
+                result = stats?.toJson();
+                xpackCallback(err, stats, resolve, reject);
+            });
+        });
+    } catch (e: any) {
+        errors.push(`[RSPACK] : ${e.message}`);
+    }
+
+    return { errors, result };
+};
+
+export const buildWithWebpack5: BundlerRunFunction = async (bundlerConfig: Configuration5) => {
+    const { default: webpack } = await import('webpack5');
+    const errors = [];
+    let result: StatsCompilation5 | undefined;
+
+    try {
+        await new Promise((resolve, reject) => {
+            webpack(bundlerConfig, (err, stats) => {
+                result = stats?.toJson();
+                xpackCallback(err, stats, resolve, reject);
+            });
+        });
+    } catch (e: any) {
+        errors.push(`[WEBPACK5] : ${e.message}`);
+    }
+
+    return { errors, result };
+};
+
+export const buildWithWebpack4: BundlerRunFunction = async (bundlerConfig: Configuration4) => {
+    const webpack = (await import('webpack4')).default;
+    const errors = [];
+    let result: Stats4.ToJsonOutput | undefined;
+
+    try {
+        await new Promise((resolve, reject) => {
+            webpack(bundlerConfig, (err, stats) => {
+                result = stats?.toJson();
+                xpackCallback(err, stats, resolve, reject, 600);
+            });
+        });
+    } catch (e: any) {
+        errors.push(`[WEBPACK4] : ${e.message}`);
+    }
+
+    return { errors, result };
+};
+
+export const buildWithEsbuild: BundlerRunFunction = async (bundlerConfigs: BuildOptions) => {
+    const { build } = await import('esbuild');
+    let result: BuildResult | undefined;
+    const errors = [];
+
+    try {
+        result = await build(bundlerConfigs);
+    } catch (e: any) {
+        errors.push(`[ESBUILD] : ${e.message}`);
+    }
+
+    return { errors, result };
+};
+
+export const buildWithVite: BundlerRunFunction = async (bundlerConfig: InlineConfig) => {
+    const vite = await import('vite');
+    const errors = [];
+    let result: Awaited<ReturnType<typeof vite.build>> | undefined;
+
+    try {
+        result = await vite.build(bundlerConfig);
+    } catch (e: any) {
+        errors.push(`[VITE] : ${e.message}`);
+    }
+
+    return { errors, result };
+};
+
+export const buildWithRollup: BundlerRunFunction = async (bundlerConfig: RollupOptions) => {
+    const { rollup } = await import('rollup');
+    const errors = [];
+    let results: RollupOutput[] | undefined;
+
+    try {
+        const result = await rollup(bundlerConfig);
+
+        // Write out the results.
+        if (bundlerConfig.output) {
+            const outputProms = [];
+            const outputOptions = Array.isArray(bundlerConfig.output)
+                ? bundlerConfig.output
+                : [bundlerConfig.output];
+            for (const outputOption of outputOptions) {
+                outputProms.push(result.write(outputOption));
+            }
+
+            results = await Promise.all(outputProms);
+        }
+    } catch (e: any) {
+        errors.push(`[ROLLUP] : ${e.message}`);
+    }
+
+    return { errors, result: results };
+};
+
+export const configXpack = (
+    config: BundlerConfig,
+): Configuration5 & Configuration4 & RspackOptions => {
+    return {
+        context: config.workingDir,
+        entry: config.entry,
+        mode: 'production',
+        output: {
+            path: config.outDir,
+            filename: `[name].js`,
+        },
+        devtool: 'source-map',
+        optimization: {
+            minimize: false,
+        },
+        plugins: config.plugins,
+    };
+};
+
+const configRollupBase = (config: BundlerConfig): RollupOptions => {
+    // Rollup doesn't have a working dir option.
+    // So we change the entry name to include the working dir.
+    const input: RollupOptions['input'] = {};
+    for (const [name, entry] of Object.entries(config.entry)) {
+        input[name] = path.resolve(config.workingDir, entry);
+    }
+
+    return {
+        input,
+        onwarn: (warning, handler) => {
+            if (
+                !/Circular dependency:/.test(warning.message) &&
+                !/Sourcemap is likely to be incorrect/.test(warning.message)
+            ) {
+                return handler(warning);
+            }
+        },
+        output: {
+            chunkFileNames: 'chunk.[hash].js',
+            compact: false,
+            dir: config.outDir,
+            entryFileNames: '[name].js',
+            sourcemap: true,
+        },
+    };
+};
+
+export const configRspack = (config: BundlerConfig): RspackOptions => {
+    return configXpack(config);
+};
+
+export const configWebpack5 = (config: BundlerConfig): Configuration5 => {
+    return configXpack(config);
+};
+
+export const configWebpack4 = (config: BundlerConfig): Configuration4 => {
+    return configXpack(config);
+};
+
+export const configEsbuild = (config: BundlerConfig): BuildOptions => {
+    return {
+        absWorkingDir: config.workingDir,
+        bundle: true,
+        chunkNames: 'chunk.[hash]',
+        entryPoints: config.entry,
+        entryNames: '[name]',
+        format: 'cjs',
+        outdir: config.outDir,
+        sourcemap: true,
+        splitting: false,
+        plugins: config.plugins,
+    };
+};
+
+export const configRollup = (config: BundlerConfig): RollupOptions => {
+    const baseConfig = configRollupBase(config);
+    return {
+        ...baseConfig,
+        plugins: [
+            commonjs(),
+            nodeResolve({ preferBuiltins: true, browser: true }),
+            ...(config.plugins || []),
+        ],
+    };
+};
+
+export const configVite = (config: BundlerConfig): InlineConfig => {
+    const baseConfig = configRollupBase({
+        ...config,
+        // Remove the plugins to only have Vite ones.
+        plugins: undefined,
+    });
+
+    return {
+        root: config.workingDir,
+        build: {
+            emptyOutDir: false,
+            assetsDir: '', // Disable assets dir to simplify the test.
+            minify: false,
+            rollupOptions: baseConfig,
+        },
+        logLevel: 'silent',
+        plugins: config.plugins,
+    };
+};
+
+export const allBundlers: Record<
+    BundlerFullName,
+    { run: BundlerRunFunction; config: BundlerConfigFunction }
+> = {
+    rspack: { run: buildWithRspack, config: configRspack },
+    webpack5: { run: buildWithWebpack5, config: configWebpack5 },
+    webpack4: { run: buildWithWebpack4, config: configWebpack4 },
+    esbuild: { run: buildWithEsbuild, config: configEsbuild },
+    vite: { run: buildWithVite, config: configVite },
+    rollup: { run: buildWithRollup, config: configRollup },
+};
diff --git a/packages/tools/src/commands/create-plugin/templates.ts b/packages/tools/src/commands/create-plugin/templates.ts
index 043752b90..f0d6ba410 100644
--- a/packages/tools/src/commands/create-plugin/templates.ts
+++ b/packages/tools/src/commands/create-plugin/templates.ts
@@ -27,7 +27,7 @@ export const getFiles = (context: Context): File[] => {
                 content: (ctx) => {
                     const hooksContent = ctx.hooks.map((hook) => getHookTemplate(hook)).join('\n');
                     return outdent`
-                        import type { GlobalContext, GetPlugins } from '@dd/core/types';
+                        import type { GlobalContext, GetPlugins, Logger } from '@dd/core/types';
 
                         import { CONFIG_KEY, PLUGIN_NAME } from './constants';
                         import type { OptionsWith${pascalCase}, ${pascalCase}Options, ${pascalCase}OptionsWithDefaults } from './types';
@@ -56,9 +56,8 @@ export const getFiles = (context: Context): File[] => {
                         export const getPlugins: GetPlugins<OptionsWith${pascalCase}> = (
                             opts: OptionsWith${pascalCase},
                             context: GlobalContext,
+                            log: Logger,
                         ) => {
-                            const log = context.getLogger(PLUGIN_NAME);
-
                             // Verify configuration.
                             const options = validateOptions(opts);
 
@@ -99,13 +98,11 @@ export const getFiles = (context: Context): File[] => {
             content: (ctx) => {
                 const hooksContent = ctx.hooks.map((hook) => getHookTemplate(hook)).join('\n');
                 return outdent`
-                    import type { GlobalContext, PluginOptions } from '@dd/core/types';
+                    import type { Logger, PluginOptions } from '@dd/core/types';
 
                     import { PLUGIN_NAME } from './constants';
 
-                    export const get${pascalCase}Plugins = (context: GlobalContext): PluginOptions[] => {
-                        const log = context.getLogger(PLUGIN_NAME);
-
+                    export const get${pascalCase}Plugins = (log: Logger): PluginOptions[] => {
                         return [
                             {
                                 name: PLUGIN_NAME,
diff --git a/packages/tools/src/commands/dev-server/index.ts b/packages/tools/src/commands/dev-server/index.ts
new file mode 100644
index 000000000..7c29194cf
--- /dev/null
+++ b/packages/tools/src/commands/dev-server/index.ts
@@ -0,0 +1,173 @@
+// 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 { FULL_NAME_BUNDLERS } from '@dd/core/constants';
+import { ROOT } from '@dd/tools/constants';
+import chalk from 'chalk';
+import { Command, Option } from 'clipanion';
+import fs from 'fs';
+import http from 'http';
+import template from 'lodash.template';
+import path from 'path';
+
+const MIME_TYPES = {
+    default: 'application/octet-stream',
+    html: 'text/html; charset=UTF-8',
+    js: 'application/javascript',
+    css: 'text/css',
+    png: 'image/png',
+    jpg: 'image/jpg',
+    gif: 'image/gif',
+    ico: 'image/x-icon',
+    svg: 'image/svg+xml',
+} as const;
+
+// Some context to use for templating content with {{something}}.
+const CONTEXT: Record<string, readonly string[]> = {
+    bundler: FULL_NAME_BUNDLERS,
+};
+
+// Templating regex.
+const INTERPOLATE_RX = /{{([\s\S]+?)}}/g;
+
+// Promise to boolean.
+const toBool = [() => true, () => false];
+
+type File = {
+    found: boolean;
+    ext: keyof typeof MIME_TYPES;
+    content: string;
+};
+
+class DevServer extends Command {
+    static paths = [['dev-server']];
+
+    static usage = Command.Usage({
+        category: `Contribution`,
+        description: `Run a basic dev server over a specific directory.`,
+        details: `
+            This command will change the package.json values of "exports" so they can be used from another project.
+
+            This is necessary to be sure that the outside project loads the built files and not the dev files.
+        `,
+        examples: [
+            [`Prepare for link`, `$0 prepare-link`],
+            [`Revert change`, `$0 prepare-link --revert`],
+        ],
+    });
+
+    port = Option.String('--port', '8000', {
+        description: 'On which port will the server run.',
+    });
+
+    root = Option.String('--root', ROOT, {
+        description: 'The root directory the server will serve.',
+    });
+
+    parseCookie(cookieHeader?: string): Record<string, string> {
+        if (!cookieHeader) {
+            return {};
+        }
+
+        const cookieString = cookieHeader
+            .split(';')
+            .find((c) => c.trim().startsWith('context_cookie='));
+
+        if (!cookieString) {
+            return {};
+        }
+
+        const [name, ...rest] = cookieString.split('=');
+        if (!name || !name.trim()) {
+            return {};
+        }
+
+        const value = rest.join('=').trim();
+        if (!value) {
+            return {};
+        }
+
+        try {
+            return JSON.parse(decodeURIComponent(value));
+        } catch (e: any) {
+            throw new Error(`Error parsing cookie: ${e.message}`);
+        }
+    }
+
+    getContext(req: http.IncomingMessage): Record<string, string> {
+        const url = new URL(req.url || '/', 'http://127.0.0.1');
+        // Get the initial context from the cookie.
+        const fileContext: Record<string, string> = this.parseCookie(req.headers.cookie);
+
+        // Verify if we have context passed as parameters (?context_bundler=vite).
+        for (const [key, value] of url.searchParams) {
+            if (key.startsWith('context_')) {
+                const contextKey = key.replace(/^context_/, '') as keyof typeof CONTEXT;
+                if (Object.keys(CONTEXT).includes(contextKey)) {
+                    if (CONTEXT[contextKey].includes(value)) {
+                        fileContext[contextKey] = value;
+                    }
+                }
+            }
+        }
+
+        return fileContext;
+    }
+
+    async prepareFile(requestUrl: string, context: Record<string, string>): Promise<File> {
+        const staticPath = this.root
+            ? path.isAbsolute(this.root)
+                ? this.root
+                : path.resolve(ROOT, this.root)
+            : ROOT;
+        const url = new URL(requestUrl, 'http://127.0.0.1');
+        const paths = [staticPath, url.pathname];
+
+        if (url.pathname.endsWith('/')) {
+            paths.push('index.html');
+        }
+
+        const filePath = path.join(...paths);
+        const pathTraversal = !filePath.startsWith(staticPath);
+        const exists = await fs.promises.access(filePath).then(...toBool);
+        const found = !pathTraversal && exists;
+        const finalPath = found ? filePath : `${staticPath}/404.html`;
+        const ext = path.extname(finalPath).substring(1).toLowerCase() as File['ext'];
+        const fileContent = template(await fs.promises.readFile(finalPath, { encoding: 'utf-8' }), {
+            interpolate: INTERPOLATE_RX,
+        })(context);
+
+        return { found, ext, content: fileContent };
+    }
+
+    async execute() {
+        http.createServer(async (req, res) => {
+            try {
+                const context = this.getContext(req);
+                const file = await this.prepareFile(req.url || '/', context);
+                const statusCode = file.found ? 200 : 404;
+                const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default;
+                const c = statusCode === 200 ? chalk.green : chalk.yellow.bold;
+
+                res.writeHead(statusCode, {
+                    'Set-Cookie': `context_cookie=${encodeURIComponent(JSON.stringify(context))};SameSite=Strict;`,
+                    'Content-Type': mimeType,
+                });
+
+                res.end(file.content);
+
+                console.log(`  -> [${c(statusCode.toString())}] ${req.method} ${req.url}`);
+            } catch (e: any) {
+                res.writeHead(500, { 'Content-Type': MIME_TYPES.html });
+                res.end('Internal Server Error');
+                const c = chalk.red.bold;
+                console.log(`  -> [${c('500')}] ${req.method} ${req.url}: ${e.message}`);
+                console.log(e);
+            }
+        }).listen(this.port);
+        console.log(`Server running at http://127.0.0.1:${this.port}/`);
+    }
+}
+
+export default [DevServer];
diff --git a/packages/tools/src/commands/integrity/readme.ts b/packages/tools/src/commands/integrity/readme.ts
index 6024f41d5..6641e99e0 100644
--- a/packages/tools/src/commands/integrity/readme.ts
+++ b/packages/tools/src/commands/integrity/readme.ts
@@ -24,7 +24,7 @@ import {
 } from '@dd/tools/helpers';
 import type { Workspace } from '@dd/tools/types';
 import fs from 'fs';
-import glob from 'glob';
+import { glob } from 'glob';
 import { outdent } from 'outdent';
 import path from 'path';
 
@@ -139,12 +139,17 @@ const getPluginTemplate = (plugin: Workspace, pluginMeta: PluginMetadata) => {
     const configContent = pluginMeta.config
         ? outdent`
 
+            <details>
+
+            <summary>Configuration</summary>
+
             \`\`\`typescript
             datadogWebpackPlugin({
             ${pluginMeta.config.replace(/;/g, ',')}
             });
             \`\`\`
 
+            </details>
         `
         : '';
 
@@ -152,8 +157,9 @@ const getPluginTemplate = (plugin: Workspace, pluginMeta: PluginMetadata) => {
         ${titleContent}${bundlerContent ? ` ${bundlerContent}` : ''}
 
         > ${intro.split('\n').join('\n> ')}
+
+        #### [📝 Full documentation ➡️](/${plugin.location}#readme)
         ${configContent}
-        <kbd>[📝 Full documentation ➡️](/${plugin.location}#readme)</kbd>
     `;
 };
 
@@ -168,26 +174,14 @@ const getBundlerMeta = (bundler: Workspace): BundlerMetadata => {
     // Catch installation and usage.
     // Everything between "## (Installation|Usage)" and the next "##".
     const installation = readme.match(/## Installation\s*((!?[\s\S](?!##))*)/)?.[1] || '';
-    const usage = readme.match(/## Usage\s*((!?[\s\S](?!##))*)/)?.[1] || '';
+    const usage = readme.match(/## Usage\s*((!?[\s\S](?!```\n))+\n```)/)?.[1] || '';
 
     return { title, name: title.toLowerCase(), usage, installation };
 };
 
 const getBundlerTemplate = (bundler: Workspace, bundlerMeta: BundlerMetadata) => {
-    const { title, name, installation, usage } = bundlerMeta;
-    return outdent`
-    ### ${getBundlerPicture(name)} ${title}
-
-    \`${bundler.name}\`
-
-    #### Installation
-    ${installation}
-
-    #### Usage
-    ${usage}
-
-    <kbd>[📝 More details ➡️](/${bundler.location}#readme)</kbd>
-    `;
+    const { title, name } = bundlerMeta;
+    return outdent`- [${getBundlerPicture(name)} ${title} \`${bundler.name}\`](/${bundler.location}#readme)`;
 };
 
 const handleBundler = (bundler: Workspace, index: number) => {
@@ -300,7 +294,7 @@ const handlePlugin = async (plugin: Workspace) => {
 const getGlobalContextType = () => {
     // Will capture the first code block after '## Global Context' up to the next title '## '.
     const RX =
-        /## Global Context(!?[\s\S](?!```typescript))+[\s\S](?<type>```typescript([\s\S](?!## ))+)/gm;
+        /## Global Context(!?[\s\S](?!```typescript))+[\s\S](?<type>```typescript([\s\S](?!```\n))+\n```)/gm;
     const coreReadmeContent = fs.readFileSync(
         path.resolve(ROOT, './packages/factory/README.md'),
         'utf-8',
@@ -364,7 +358,7 @@ export const updateReadmes = async (plugins: Workspace[], bundlers: Workspace[])
     rootReadmeContent = replaceInBetween(
         rootReadmeContent,
         MD_BUNDLERS_KEY,
-        bundlersContents.join('\n\n'),
+        bundlersContents.join('\n'),
     );
     rootReadmeContent = replaceInBetween(
         rootReadmeContent,
diff --git a/packages/tools/src/commands/oss/apply.ts b/packages/tools/src/commands/oss/apply.ts
index 93f1d98ff..b45fb159e 100644
--- a/packages/tools/src/commands/oss/apply.ts
+++ b/packages/tools/src/commands/oss/apply.ts
@@ -6,7 +6,7 @@ import checkbox from '@inquirer/checkbox';
 import select from '@inquirer/select';
 import chalk from 'chalk';
 import fs from 'fs';
-import glob from 'glob';
+import { glob } from 'glob';
 import path from 'path';
 
 import { NAME, ROOT } from '../../constants';
diff --git a/packages/tools/src/helpers.ts b/packages/tools/src/helpers.ts
index 9efaa9bbc..903ce6a63 100644
--- a/packages/tools/src/helpers.ts
+++ b/packages/tools/src/helpers.ts
@@ -3,8 +3,17 @@
 // Copyright 2019-Present Datadog, Inc.
 
 import { ALL_BUNDLERS, SUPPORTED_BUNDLERS } from '@dd/core/constants';
-import { readJsonSync } from '@dd/core/helpers';
-import type { GetPlugins, Logger } from '@dd/core/types';
+import { outputJsonSync, readJsonSync, serializeBuildReport } from '@dd/core/helpers';
+import type {
+    BuildReport,
+    BundlerFullName,
+    BundlerName,
+    GetCustomPlugins,
+    GetPlugins,
+    GlobalContext,
+    IterableElement,
+    Logger,
+} from '@dd/core/types';
 import chalk from 'chalk';
 import { execFile, execFileSync } from 'child_process';
 import path from 'path';
@@ -18,6 +27,7 @@ export const yellow = chalk.bold.yellow;
 export const grey = chalk.bold.grey;
 export const red = chalk.bold.red;
 export const bgYellow = chalk.bold.bgYellow.black;
+export const bgGreen = chalk.bold.bgGreen.black;
 export const blue = chalk.bold.cyan;
 export const bold = chalk.bold;
 export const dim = chalk.dim;
@@ -47,7 +57,10 @@ export const slugify = (string: string) => {
 export const replaceInBetween = (content: string, mark: string, injection: string) => {
     const escapedMark = mark.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
     const escapedInjection = injection.replace(/\$/g, '$$$$');
-    const rx = new RegExp(`${escapedMark}[\\S\\s]*${escapedMark}`, 'gm');
+    const rx = new RegExp(
+        `${escapedMark}([\\S\\s](?!${escapedMark}))*(\\s|\\S)?${escapedMark}`,
+        'gm',
+    );
     return content.replace(rx, `${mark}\n${escapedInjection}\n${mark}`);
 };
 
@@ -111,6 +124,21 @@ export const runAutoFixes = async () => {
     return errors;
 };
 
+export const buildPlugins = (bundlerNames: (BundlerName | BundlerFullName)[]) => {
+    const bundlersToBuild = Array.from(
+        new Set(bundlerNames.map((name) => name.replace(/\d/g, ''))),
+    );
+
+    return executeSync('yarn', [
+        'workspaces',
+        'foreach',
+        '-Apti',
+        ...bundlersToBuild.map((bundler) => ['--include', `@datadog/${bundler}-plugin`]).flat(),
+        'run',
+        'build',
+    ]);
+};
+
 export const getWorkspaces = async (
     filter?: (workspace: SlugLessWorkspace) => boolean,
 ): Promise<Workspace[]> => {
@@ -131,10 +159,15 @@ export const getWorkspaces = async (
 
 // TODO: Update this, it's a bit hacky.
 export const getSupportedBundlers = (getPlugins: GetPlugins<any>) => {
+    const bundler: BuildReport['bundler'] = {
+        name: 'esbuild',
+        fullName: 'esbuild',
+        version: '1.0.0',
+    };
     const plugins = getPlugins(
         {
             telemetry: {},
-            rum: {
+            errorTracking: {
                 sourcemaps: {
                     releaseVersion: '0',
                     service: 'service',
@@ -147,15 +180,14 @@ export const getSupportedBundlers = (getPlugins: GetPlugins<any>) => {
             version: '0',
             start: 0,
             bundler: {
-                name: 'esbuild',
-                fullName: 'esbuild',
+                ...bundler,
                 outDir: ROOT,
-                version: '1.0.0',
             },
             build: {
                 warnings: [],
                 errors: [],
                 logs: [],
+                bundler,
             },
             inject() {},
             pluginNames: [],
@@ -217,3 +249,79 @@ export const getBundlerPicture = (bundler: string) => {
 
 export const isInternalPluginWorkspace = (workspace: Workspace) =>
     workspace.name.startsWith('@dd/internal-');
+
+// Returns a customPlugin to output some debug files.
+type CustomPlugins = ReturnType<GetCustomPlugins>;
+export const debugFilesPlugins = (context: GlobalContext): CustomPlugins => {
+    const rollupPlugin: IterableElement<CustomPlugins>['rollup'] = {
+        writeBundle(options, bundle) {
+            outputJsonSync(
+                path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`),
+                bundle,
+            );
+        },
+    };
+
+    const xpackPlugin: IterableElement<CustomPlugins>['webpack'] &
+        IterableElement<CustomPlugins>['rspack'] = (compiler) => {
+        type Stats = Parameters<Parameters<typeof compiler.hooks.done.tap>[1]>[0];
+
+        compiler.hooks.done.tap('bundler-outputs', (stats: Stats) => {
+            const statsJson = stats.toJson({
+                all: false,
+                assets: true,
+                children: true,
+                chunks: true,
+                chunkGroupAuxiliary: true,
+                chunkGroupChildren: true,
+                chunkGroups: true,
+                chunkModules: true,
+                chunkRelations: true,
+                entrypoints: true,
+                errors: true,
+                ids: true,
+                modules: true,
+                nestedModules: true,
+                reasons: true,
+                relatedAssets: true,
+                warnings: true,
+            });
+            outputJsonSync(
+                path.resolve(context.bundler.outDir, `output.${context.bundler.fullName}.json`),
+                statsJson,
+            );
+        });
+    };
+
+    return [
+        {
+            name: 'build-report',
+            writeBundle() {
+                outputJsonSync(
+                    path.resolve(context.bundler.outDir, `report.${context.bundler.fullName}.json`),
+                    serializeBuildReport(context.build),
+                );
+            },
+        },
+        {
+            name: 'bundler-outputs',
+            esbuild: {
+                setup(build) {
+                    build.onEnd((result) => {
+                        outputJsonSync(
+                            path.resolve(
+                                context.bundler.outDir,
+                                `output.${context.bundler.fullName}.json`,
+                            ),
+                            result.metafile,
+                        );
+                    });
+                },
+            },
+            rspack: xpackPlugin,
+            rollup: rollupPlugin,
+            vite: rollupPlugin,
+            webpack: xpackPlugin,
+        },
+    ];
+};
diff --git a/packages/tools/src/plugins.ts b/packages/tools/src/plugins.ts
new file mode 100644
index 000000000..6cdf93c9c
--- /dev/null
+++ b/packages/tools/src/plugins.ts
@@ -0,0 +1,100 @@
+// 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 { BundlerFullName, Options } from '@dd/core/types';
+import { CONFIG_KEY as ERROR_TRACKING } from '@dd/error-tracking-plugin';
+import { CONFIG_KEY as TELEMETRY } from '@dd/telemetry-plugin';
+import fs from 'fs';
+import path from 'path';
+
+import { ROOT } from './constants';
+
+export const defaultConfig: Options = {
+    auth: {
+        apiKey: process.env.DATADOG_API_KEY,
+        appKey: process.env.DATADOG_APP_KEY,
+    },
+};
+
+export const fullConfig: Options = {
+    ...defaultConfig,
+    [ERROR_TRACKING]: {
+        sourcemaps: {
+            bailOnError: false,
+            dryRun: false,
+            maxConcurrency: 10,
+            minifiedPathPrefix: '/',
+            releaseVersion: '1.0.0',
+            service: 'error-tracking-build-plugin-sourcemaps',
+        },
+    },
+    [TELEMETRY]: {
+        enableTracing: true,
+        timestamp: new Date().getTime(),
+    },
+};
+
+// We load the plugins dynamically to avoid esm/cjs issues.
+// Using '*/dist/src' to specifically target the bundled files.
+
+export const getEsbuildPlugin = (config: Options) => {
+    // eslint-disable-next-line import/no-unresolved
+    const { datadogEsbuildPlugin } = require('@datadog/esbuild-plugin/dist/src');
+    return datadogEsbuildPlugin(config);
+};
+
+export const getRollupPlugin = (config: Options) => {
+    // eslint-disable-next-line import/no-unresolved
+    const { datadogRollupPlugin } = require('@datadog/rollup-plugin/dist/src');
+    return datadogRollupPlugin(config);
+};
+
+export const getRspackPlugin = (config: Options) => {
+    // eslint-disable-next-line import/no-unresolved
+    const { datadogRspackPlugin } = require('@datadog/rspack-plugin/dist/src');
+    return datadogRspackPlugin(config);
+};
+
+export const getVitePlugin = (config: Options) => {
+    // eslint-disable-next-line import/no-unresolved
+    const { datadogVitePlugin } = require('@datadog/vite-plugin/dist/src');
+    return datadogVitePlugin(config);
+};
+
+export const getWebpack4Plugin = (config: Options) => {
+    // We'll write a plugin specifically for Webpack4.
+    const webpackPluginRoot = path.resolve(ROOT, 'packages/published/webpack-plugin/dist/src');
+    const webpack4PluginPath = path.resolve(webpackPluginRoot, 'index4.js');
+    const webpack5PluginPath = path.resolve(webpackPluginRoot, 'index.js');
+
+    // First verify if it exists already or not.
+    if (!fs.existsSync(webpack4PluginPath)) {
+        // Create the file with the correct imports of Webpack4.
+        fs.writeFileSync(
+            webpack4PluginPath,
+            fs
+                .readFileSync(webpack5PluginPath, { encoding: 'utf-8' })
+                .replace(/require\(('|")webpack("|')\)/g, "require('webpack4')"),
+        );
+    }
+
+    // eslint-disable-next-line import/no-unresolved
+    const { datadogWebpackPlugin } = require('@datadog/webpack-plugin/dist/src/index4.js');
+    return datadogWebpackPlugin(config);
+};
+
+export const getWebpack5Plugin = (config: Options) => {
+    // eslint-disable-next-line import/no-unresolved
+    const { datadogWebpackPlugin } = require('@datadog/webpack-plugin/dist/src/index.js');
+    return datadogWebpackPlugin(config);
+};
+
+export const allPlugins: Record<BundlerFullName, (config: Options) => any> = {
+    esbuild: getEsbuildPlugin,
+    rollup: getRollupPlugin,
+    rspack: getRspackPlugin,
+    vite: getVitePlugin,
+    webpack4: getWebpack4Plugin,
+    webpack5: getWebpack5Plugin,
+};
diff --git a/packages/tools/src/rollupConfig.mjs b/packages/tools/src/rollupConfig.mjs
index bf6d25d46..0f92613dd 100644
--- a/packages/tools/src/rollupConfig.mjs
+++ b/packages/tools/src/rollupConfig.mjs
@@ -4,20 +4,27 @@
 
 import babel from '@rollup/plugin-babel';
 import commonjs from '@rollup/plugin-commonjs';
+import esmShim from '@rollup/plugin-esm-shim';
 import json from '@rollup/plugin-json';
 import { nodeResolve } from '@rollup/plugin-node-resolve';
+import terser from '@rollup/plugin-terser';
+import chalk from 'chalk';
+import glob from 'glob';
 import modulePackage from 'module';
+import path from 'path';
 import dts from 'rollup-plugin-dts';
 import esbuild from 'rollup-plugin-esbuild';
 
+const CWD = process.env.PROJECT_CWD;
+
 /**
  * @param {{module: string; main: string;}} packageJson
  * @param {import('rollup').RollupOptions} config
  * @returns {import('rollup').RollupOptions}
  */
 export const bundle = (packageJson, config) => ({
-    ...config,
     input: 'src/index.ts',
+    ...config,
     external: [
         // All peer dependencies are external dependencies.
         ...Object.keys(packageJson.peerDependencies),
@@ -29,7 +36,15 @@ export const bundle = (packageJson, config) => ({
         '@dd/tests',
         // We never want to include Node.js built-in modules in the bundle.
         ...modulePackage.builtinModules,
+        ...(config.external || []),
     ],
+    onwarn(warning, warn) {
+        // Ignore warnings about undefined `this`.
+        if (warning.code === 'THIS_IS_UNDEFINED') {
+            return;
+        }
+        warn(warning);
+    },
     plugins: [
         babel({
             babelHelpers: 'bundled',
@@ -40,37 +55,101 @@ export const bundle = (packageJson, config) => ({
         nodeResolve({ preferBuiltins: true }),
         ...config.plugins,
     ],
-    output: {
+});
+
+/**
+ * @param {{module: string; main: string;}} packageJson
+ * @param {Partial<import('rollup').OutputOptions>} overrides
+ * @returns {import('rollup').OutputOptions}
+ */
+const getOutput = (packageJson, overrides = {}) => {
+    const filename = overrides.format === 'esm' ? packageJson.module : packageJson.main;
+    const plugins = [terser()];
+
+    // Inject ESM shims to support __dirname and co.
+    if (overrides.format === 'esm') {
+        plugins.push(esmShim());
+    }
+
+    return {
         exports: 'named',
         sourcemap: true,
-        ...config.output,
-    },
-});
+        entryFileNames: `[name]${path.extname(filename)}`,
+        dir: path.dirname(filename),
+        plugins,
+        format: 'cjs',
+        globals: {
+            globalThis: 'window',
+        },
+        // No chunks.
+        manualChunks: () => '[name]',
+        ...overrides,
+    };
+};
 
 /**
  * @param {{module: string; main: string;}} packageJson
  * @returns {import('rollup').RollupOptions[]}
  */
-export const getDefaultBuildConfigs = (packageJson) => [
-    bundle(packageJson, {
-        plugins: [esbuild()],
-        output: {
-            file: packageJson.module,
-            format: 'esm',
-        },
-    }),
-    bundle(packageJson, {
-        plugins: [esbuild()],
-        output: {
-            file: packageJson.main,
-            format: 'cjs',
-        },
-    }),
-    // FIXME: This build is sloooow.
-    bundle(packageJson, {
-        plugins: [dts()],
-        output: {
-            dir: 'dist/src',
-        },
-    }),
-];
+export const getDefaultBuildConfigs = async (packageJson) => {
+    // Verify if we have anything else to build from plugins.
+    const pkgs = glob.sync('packages/plugins/**/package.json', { cwd: CWD });
+    const pluginBuilds = [];
+    for (const pkg of pkgs) {
+        const { default: content } = await import(path.resolve(CWD, pkg), {
+            assert: { type: 'json' },
+        });
+
+        if (!content.toBuild) {
+            continue;
+        }
+
+        console.log(
+            `Will also build ${chalk.green.bold(content.name)} additional files: ${chalk.green.bold(Object.keys(content.toBuild).join(', '))}`,
+        );
+
+        pluginBuilds.push(
+            ...Object.entries(content.toBuild).map(([name, config]) => {
+                return bundle(packageJson, {
+                    plugins: [esbuild()],
+                    external: config.external,
+                    input: {
+                        [name]: path.join(CWD, path.dirname(pkg), config.entry),
+                    },
+                    output: [
+                        getOutput(packageJson, {
+                            format: 'cjs',
+                            sourcemap: false,
+                            plugins: [terser({ mangle: true })],
+                        }),
+                    ],
+                });
+            }),
+        );
+    }
+
+    const configs = [
+        // Main bundle.
+        bundle(packageJson, {
+            plugins: [esbuild()],
+            input: {
+                index: 'src/index.ts',
+            },
+            output: [
+                getOutput(packageJson, { format: 'esm' }),
+                getOutput(packageJson, { format: 'cjs' }),
+            ],
+        }),
+        ...pluginBuilds,
+        // Bundle type definitions.
+        // FIXME: This build is sloooow.
+        // Check https://github.com/timocov/dts-bundle-generator
+        bundle(packageJson, {
+            plugins: [dts()],
+            output: {
+                dir: 'dist/src',
+            },
+        }),
+    ];
+    return configs;
+};
diff --git a/yarn.lock b/yarn.lock
index b94cf79dd..f8690b8fe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1449,8 +1449,10 @@ __metadata:
     "@dd/tools": "workspace:*"
     "@rollup/plugin-babel": "npm:6.0.4"
     "@rollup/plugin-commonjs": "npm:28.0.1"
+    "@rollup/plugin-esm-shim": "npm:0.1.7"
     "@rollup/plugin-json": "npm:6.1.0"
     "@rollup/plugin-node-resolve": "npm:15.3.0"
+    "@rollup/plugin-terser": "npm:0.4.4"
     "@types/babel__core": "npm:^7"
     "@types/babel__preset-env": "npm:^7"
     async-retry: "npm:1.3.3"
@@ -1482,8 +1484,10 @@ __metadata:
     "@dd/tools": "workspace:*"
     "@rollup/plugin-babel": "npm:6.0.4"
     "@rollup/plugin-commonjs": "npm:28.0.1"
+    "@rollup/plugin-esm-shim": "npm:0.1.7"
     "@rollup/plugin-json": "npm:6.1.0"
     "@rollup/plugin-node-resolve": "npm:15.3.0"
+    "@rollup/plugin-terser": "npm:0.4.4"
     "@types/babel__core": "npm:^7"
     "@types/babel__preset-env": "npm:^7"
     async-retry: "npm:1.3.3"
@@ -1504,7 +1508,7 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@datadog/rspack-plugin@workspace:packages/published/rspack-plugin":
+"@datadog/rspack-plugin@workspace:*, @datadog/rspack-plugin@workspace:packages/published/rspack-plugin":
   version: 0.0.0-use.local
   resolution: "@datadog/rspack-plugin@workspace:packages/published/rspack-plugin"
   dependencies:
@@ -1515,8 +1519,10 @@ __metadata:
     "@dd/tools": "workspace:*"
     "@rollup/plugin-babel": "npm:6.0.4"
     "@rollup/plugin-commonjs": "npm:28.0.1"
+    "@rollup/plugin-esm-shim": "npm:0.1.7"
     "@rollup/plugin-json": "npm:6.1.0"
     "@rollup/plugin-node-resolve": "npm:15.3.0"
+    "@rollup/plugin-terser": "npm:0.4.4"
     "@types/babel__core": "npm:^7"
     "@types/babel__preset-env": "npm:^7"
     async-retry: "npm:1.3.3"
@@ -1548,8 +1554,10 @@ __metadata:
     "@dd/tools": "workspace:*"
     "@rollup/plugin-babel": "npm:6.0.4"
     "@rollup/plugin-commonjs": "npm:28.0.1"
+    "@rollup/plugin-esm-shim": "npm:0.1.7"
     "@rollup/plugin-json": "npm:6.1.0"
     "@rollup/plugin-node-resolve": "npm:15.3.0"
+    "@rollup/plugin-terser": "npm:0.4.4"
     "@types/babel__core": "npm:^7"
     "@types/babel__preset-env": "npm:^7"
     async-retry: "npm:1.3.3"
@@ -1581,8 +1589,10 @@ __metadata:
     "@dd/tools": "workspace:*"
     "@rollup/plugin-babel": "npm:6.0.4"
     "@rollup/plugin-commonjs": "npm:28.0.1"
+    "@rollup/plugin-esm-shim": "npm:0.1.7"
     "@rollup/plugin-json": "npm:6.1.0"
     "@rollup/plugin-node-resolve": "npm:15.3.0"
+    "@rollup/plugin-terser": "npm:0.4.4"
     "@types/babel__core": "npm:^7"
     "@types/babel__preset-env": "npm:^7"
     async-retry: "npm:1.3.3"
@@ -1618,21 +1628,34 @@ __metadata:
     "@types/node": "npm:^18"
     async-retry: "npm:1.3.3"
     chalk: "npm:2.3.1"
+    esbuild: "npm:0.24.0"
+    glob: "npm:11.0.0"
     typescript: "npm:5.4.3"
     unplugin: "npm:1.16.0"
   languageName: unknown
   linkType: soft
 
+"@dd/error-tracking-plugin@workspace:*, @dd/error-tracking-plugin@workspace:packages/plugins/error-tracking":
+  version: 0.0.0-use.local
+  resolution: "@dd/error-tracking-plugin@workspace:packages/plugins/error-tracking"
+  dependencies:
+    "@dd/core": "workspace:*"
+    chalk: "npm:2.3.1"
+    outdent: "npm:0.8.0"
+    p-queue: "npm:6.6.2"
+  languageName: unknown
+  linkType: soft
+
 "@dd/factory@workspace:*, @dd/factory@workspace:packages/factory":
   version: 0.0.0-use.local
   resolution: "@dd/factory@workspace:packages/factory"
   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:*"
     "@dd/internal-injection-plugin": "workspace:*"
-    "@dd/rum-plugin": "workspace:*"
     "@dd/telemetry-plugin": "workspace:*"
     chalk: "npm:2.3.1"
     unplugin: "npm:1.16.0"
@@ -1644,7 +1667,6 @@ __metadata:
   resolution: "@dd/internal-build-report-plugin@workspace:packages/plugins/build-report"
   dependencies:
     "@dd/core": "workspace:*"
-    glob: "npm:11.0.0"
   languageName: unknown
   linkType: soft
 
@@ -1674,17 +1696,6 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@dd/rum-plugin@workspace:*, @dd/rum-plugin@workspace:packages/plugins/rum":
-  version: 0.0.0-use.local
-  resolution: "@dd/rum-plugin@workspace:packages/plugins/rum"
-  dependencies:
-    "@dd/core": "workspace:*"
-    chalk: "npm:2.3.1"
-    outdent: "npm:0.8.0"
-    p-queue: "npm:6.6.2"
-  languageName: unknown
-  linkType: soft
-
 "@dd/telemetry-plugin@workspace:*, @dd/telemetry-plugin@workspace:packages/plugins/telemetry":
   version: 0.0.0-use.local
   resolution: "@dd/telemetry-plugin@workspace:packages/plugins/telemetry"
@@ -1712,15 +1723,18 @@ __metadata:
   dependencies:
     "@datadog/esbuild-plugin": "workspace:*"
     "@datadog/rollup-plugin": "workspace:*"
+    "@datadog/rspack-plugin": "workspace:*"
     "@datadog/vite-plugin": "workspace:*"
     "@datadog/webpack-plugin": "workspace:*"
     "@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:*"
     "@dd/internal-injection-plugin": "workspace:*"
-    "@dd/rum-plugin": "workspace:*"
     "@dd/telemetry-plugin": "workspace:*"
+    "@dd/tools": "workspace:*"
+    "@playwright/test": "npm:1.49.1"
     "@rollup/plugin-commonjs": "npm:28.0.1"
     "@rspack/core": "npm:1.1.2"
     "@types/faker": "npm:5.5.9"
@@ -1749,21 +1763,40 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@dd/tools@workspace:packages/tools"
   dependencies:
+    "@datadog/esbuild-plugin": "workspace:*"
+    "@datadog/rollup-plugin": "workspace:*"
+    "@datadog/rspack-plugin": "workspace:*"
+    "@datadog/vite-plugin": "workspace:*"
+    "@datadog/webpack-plugin": "workspace:*"
     "@dd/assets": "workspace:*"
     "@dd/core": "workspace:*"
+    "@dd/error-tracking-plugin": "workspace:*"
+    "@dd/telemetry-plugin": "workspace:*"
     "@inquirer/checkbox": "npm:2.3.3"
     "@inquirer/input": "npm:2.1.7"
     "@inquirer/select": "npm:2.3.3"
+    "@rollup/plugin-commonjs": "npm:28.0.1"
+    "@rollup/plugin-esm-shim": "npm:0.1.7"
+    "@rollup/plugin-node-resolve": "npm:15.3.0"
+    "@rspack/core": "npm:1.1.2"
     "@types/chalk": "npm:2.2.0"
     "@types/glob": "npm:7.1.4"
+    "@types/lodash.template": "npm:^4"
     "@types/node": "npm:^18"
+    "@types/webpack4": "npm:@types/webpack@4.41.38"
     chalk: "npm:2.3.1"
     clipanion: "npm:4.0.0-rc.3"
+    esbuild: "npm:0.24.0"
     glob: "npm:7.1.6"
+    lodash.template: "npm:4.5.0"
     outdent: "npm:0.8.0"
     ts-node: "npm:10.9.2"
     typanion: "npm:3.14.0"
     typescript: "npm:5.4.3"
+    vite: "npm:5.4.10"
+    webpack: "npm:5.92.1"
+    webpack4: "npm:webpack@4.47.0"
+    webpack5: "npm:webpack@5.92.1"
   languageName: unknown
   linkType: soft
 
@@ -2685,6 +2718,17 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@playwright/test@npm:1.49.1":
+  version: 1.49.1
+  resolution: "@playwright/test@npm:1.49.1"
+  dependencies:
+    playwright: "npm:1.49.1"
+  bin:
+    playwright: cli.js
+  checksum: 10/bb0d5eda58ee0b5bbca732d2aa57782fadf420d101e08e16d5760179459c667907bd8d224ee3d6f43f3088378e377ef63d32ed605fec37605debf217c3efe8da
+  languageName: node
+  linkType: hard
+
 "@rollup/plugin-babel@npm:6.0.4":
   version: 6.0.4
   resolution: "@rollup/plugin-babel@npm:6.0.4"
@@ -2724,6 +2768,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/plugin-esm-shim@npm:0.1.7":
+  version: 0.1.7
+  resolution: "@rollup/plugin-esm-shim@npm:0.1.7"
+  dependencies:
+    magic-string: "npm:^0.30.3"
+  peerDependencies:
+    rollup: ^2.0.0||^3.0.0||^4.0.0
+  peerDependenciesMeta:
+    rollup:
+      optional: true
+  checksum: 10/c3cc762ce729d2f87b9b2d4d3e90af49dc91036860cca95dd103d175cf5425e72068219d7300d2a006b0408cb0a9575a8c79f1534e8625663ed996d9ea58b397
+  languageName: node
+  linkType: hard
+
 "@rollup/plugin-json@npm:6.1.0":
   version: 6.1.0
   resolution: "@rollup/plugin-json@npm:6.1.0"
@@ -2756,6 +2814,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rollup/plugin-terser@npm:0.4.4":
+  version: 0.4.4
+  resolution: "@rollup/plugin-terser@npm:0.4.4"
+  dependencies:
+    serialize-javascript: "npm:^6.0.1"
+    smob: "npm:^1.0.0"
+    terser: "npm:^5.17.4"
+  peerDependencies:
+    rollup: ^2.0.0||^3.0.0||^4.0.0
+  peerDependenciesMeta:
+    rollup:
+      optional: true
+  checksum: 10/a5e066ddea55fc8c32188bc8b484cca619713516f10e3a06801881ec98bf37459ca24e5fe8711f93a5fa7f26a6e9132a47bc1a61c01e0b513dfd79a96cdc6eb7
+  languageName: node
+  linkType: hard
+
 "@rollup/pluginutils@npm:^5.0.1, @rollup/pluginutils@npm:^5.0.5, @rollup/pluginutils@npm:^5.1.0":
   version: 5.1.0
   resolution: "@rollup/pluginutils@npm:5.1.0"
@@ -3258,6 +3332,22 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/lodash.template@npm:^4":
+  version: 4.5.3
+  resolution: "@types/lodash.template@npm:4.5.3"
+  dependencies:
+    "@types/lodash": "npm:*"
+  checksum: 10/7c9d32b8d81959c8131223da191d2dd55d8ddfd4f0997bf18565b65cb6ba3b8087d39f458071031494b23f5dac857b9cbdba00df8b32c2d8178e89478b0b44ee
+  languageName: node
+  linkType: hard
+
+"@types/lodash@npm:*":
+  version: 4.17.14
+  resolution: "@types/lodash@npm:4.17.14"
+  checksum: 10/6ee40725f3e192f5ef1f493caca19210aa7acd7adc3136b8dba84d418a35be0abea0668105aed9f696ad62a54310a9c0d328971ad4b157f5bcda700424ed5aae
+  languageName: node
+  linkType: hard
+
 "@types/minimatch@npm:*":
   version: 5.1.2
   resolution: "@types/minimatch@npm:5.1.2"
@@ -6749,6 +6839,16 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fsevents@npm:2.3.2":
+  version: 2.3.2
+  resolution: "fsevents@npm:2.3.2"
+  dependencies:
+    node-gyp: "npm:latest"
+  checksum: 10/6b5b6f5692372446ff81cf9501c76e3e0459a4852b3b5f1fc72c103198c125a6b8c72f5f166bdd76ffb2fca261e7f6ee5565daf80dca6e571e55bcc589cc1256
+  conditions: os=darwin
+  languageName: node
+  linkType: hard
+
 "fsevents@npm:^1.2.7":
   version: 1.2.13
   resolution: "fsevents@npm:1.2.13"
@@ -6770,6 +6870,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
+  version: 2.3.2
+  resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
+  dependencies:
+    node-gyp: "npm:latest"
+  conditions: os=darwin
+  languageName: node
+  linkType: hard
+
 "fsevents@patch:fsevents@npm%3A^1.2.7#optional!builtin<compat/fsevents>":
   version: 1.2.13
   resolution: "fsevents@patch:fsevents@npm%3A1.2.13#optional!builtin<compat/fsevents>::version=1.2.13&hash=d11327"
@@ -8638,6 +8747,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lodash._reinterpolate@npm:^3.0.0":
+  version: 3.0.0
+  resolution: "lodash._reinterpolate@npm:3.0.0"
+  checksum: 10/06d2d5f33169604fa5e9f27b6067ed9fb85d51a84202a656901e5ffb63b426781a601508466f039c720af111b0c685d12f1a5c14ff8df5d5f27e491e562784b2
+  languageName: node
+  linkType: hard
+
 "lodash.debounce@npm:^4.0.8":
   version: 4.0.8
   resolution: "lodash.debounce@npm:4.0.8"
@@ -8659,6 +8775,25 @@ __metadata:
   languageName: node
   linkType: hard
 
+"lodash.template@npm:4.5.0":
+  version: 4.5.0
+  resolution: "lodash.template@npm:4.5.0"
+  dependencies:
+    lodash._reinterpolate: "npm:^3.0.0"
+    lodash.templatesettings: "npm:^4.0.0"
+  checksum: 10/56d18ba410ff591f22e4dd2974d21fdcfcba392f2d462ee4b7a7368c3a28ac1cb38a73f1d1c9eb8b8cae26f8e0ae2c28058f7488b4ffa9da84a6096bc77691db
+  languageName: node
+  linkType: hard
+
+"lodash.templatesettings@npm:^4.0.0":
+  version: 4.2.0
+  resolution: "lodash.templatesettings@npm:4.2.0"
+  dependencies:
+    lodash._reinterpolate: "npm:^3.0.0"
+  checksum: 10/ef470fa8b66b6370b08fb0709c1577e4bf72cc3d1e8639196577db827915808ec138861cbc791b295a24fbfe7b78dd26bcfc8f237e5d94df383a3125ae6f5339
+  languageName: node
+  linkType: hard
+
 "log-symbols@npm:^3.0.0":
   version: 3.0.0
   resolution: "log-symbols@npm:3.0.0"
@@ -9711,6 +9846,30 @@ __metadata:
   languageName: node
   linkType: hard
 
+"playwright-core@npm:1.49.1":
+  version: 1.49.1
+  resolution: "playwright-core@npm:1.49.1"
+  bin:
+    playwright-core: cli.js
+  checksum: 10/baa39a53024ec7744708410f2b952ac3aa2e1a6d311dabfa303523712848eba142fce5c20f1b2ed2a66fbd9a415d22ea8642b0f70423360aaebd4b41c47d364e
+  languageName: node
+  linkType: hard
+
+"playwright@npm:1.49.1":
+  version: 1.49.1
+  resolution: "playwright@npm:1.49.1"
+  dependencies:
+    fsevents: "npm:2.3.2"
+    playwright-core: "npm:1.49.1"
+  dependenciesMeta:
+    fsevents:
+      optional: true
+  bin:
+    playwright: cli.js
+  checksum: 10/49fb063f4a107b8090f66d2d351ebd51fbb66843a8f95a161fa0c0e0b5156515961e75cc10f4249f61b9d2af51f762dda505c62b096d8f61cd47d1ff73ab39d2
+  languageName: node
+  linkType: hard
+
 "please-upgrade-node@npm:^3.2.0":
   version: 3.2.0
   resolution: "please-upgrade-node@npm:3.2.0"
@@ -9802,14 +9961,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"project@workspace:packages/tests/src/_jest/fixtures/project":
-  version: 0.0.0-use.local
-  resolution: "project@workspace:packages/tests/src/_jest/fixtures/project"
-  dependencies:
-    chalk: "npm:2.3.1"
-  languageName: unknown
-  linkType: soft
-
 "promise-inflight@npm:^1.0.1":
   version: 1.0.1
   resolution: "promise-inflight@npm:1.0.1"
@@ -10700,6 +10851,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"smob@npm:^1.0.0":
+  version: 1.5.0
+  resolution: "smob@npm:1.5.0"
+  checksum: 10/a1ea453bcea89989062626ea30a1fcb42c62e96255619c8641ffa1d7ab42baf415975c67c718127036901b9e487d8bf4c46219e50cec54295412c1227700b8fe
+  languageName: node
+  linkType: hard
+
 "snapdragon-node@npm:^2.0.1":
   version: 2.1.1
   resolution: "snapdragon-node@npm:2.1.1"
@@ -11221,6 +11379,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"terser@npm:^5.17.4":
+  version: 5.37.0
+  resolution: "terser@npm:5.37.0"
+  dependencies:
+    "@jridgewell/source-map": "npm:^0.3.3"
+    acorn: "npm:^8.8.2"
+    commander: "npm:^2.20.0"
+    source-map-support: "npm:~0.5.20"
+  bin:
+    terser: bin/terser
+  checksum: 10/3afacf7c38c47a5a25dbe1ba2e7aafd61166474d4377ec0af490bd41ab3686ab12679818d5fe4a3e7f76efee26f639c92ac334940c378bbc31176520a38379c3
+  languageName: node
+  linkType: hard
+
 "terser@npm:^5.26.0":
   version: 5.29.2
   resolution: "terser@npm:5.29.2"