diff --git a/README.md b/README.md
index 9fb91758..7bda1a9f 100644
--- a/README.md
+++ b/README.md
@@ -22,10 +22,9 @@ A set of plugins to interact with Datadog directly from your builds.
- [Webpack](#-webpack)
- [Features](#features)
- [RUM](#rum----)
- - [Telemetry](#telemetry--)
+ - [Telemetry](#telemetry----)
- [Configuration](#configuration)
- [`auth.apiKey`](#authapikey)
- - [`auth.endPoint`](#authendpoint)
- [`logLevel`](#loglevel)
- [Contributing](#contributing)
- [License](#license)
@@ -216,7 +215,7 @@ datadogWebpackPlugin({
[📝 Full documentation ➡️](./packages/plugins/rum#readme)
-### Telemetry
+### Telemetry
> Display and send telemetry data as metrics to Datadog.
@@ -224,15 +223,14 @@ datadogWebpackPlugin({
datadogWebpackPlugin({
telemetry?: {
disabled?: boolean,
+ enableTracing?: boolean,
+ endPoint?: string,
output?: boolean
| string
| {
destination: string,
timings?: boolean,
- dependencies?: boolean,
- bundler?: boolean,
metrics?: boolean,
- logs?: boolean,
},
prefix?: string,
tags?: string[],
@@ -268,15 +266,14 @@ datadogWebpackPlugin({
};
telemetry?: {
disabled?: boolean;
+ enableTracing?: boolean;
+ endPoint?: string;
output?: boolean
| string
| {
destination: string;
timings?: boolean;
- dependencies?: boolean;
- bundler?: boolean;
metrics?: boolean;
- logs?: boolean;
};
prefix?: string;
tags?: string[];
@@ -293,12 +290,6 @@ datadogWebpackPlugin({
In order to interact with Datadog, you have to use [your own API Key](https://app.datadoghq.com/organization-settings/api-keys).
-### `auth.endPoint`
-
-> default: `"app.datadoghq.com"`
-
-To which endpoint will the metrics be sent.
-
### `logLevel`
> default: `'warn'`
diff --git a/lint-staged.config.js b/lint-staged.config.js
index 707a129e..a02583c2 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -3,6 +3,11 @@
// Copyright 2019-Present Datadog, Inc.
module.exports = {
- '*.{ts,tsx}': () => ['yarn typecheck:all', 'yarn format', 'git add'],
+ '*.{ts,tsx}': (filenames) => [
+ 'yarn typecheck:all',
+ `eslint ${filenames.join(' ')} --quiet --fix`,
+ 'git add',
+ ],
+ '*.{js,mjs}': (filenames) => [`eslint ${filenames.join(' ')} --quiet --fix`, 'git add'],
relative: 'true',
};
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 7a916e80..2cfa425d 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -17,7 +17,7 @@ import type { UnpluginContextMeta, UnpluginOptions } from 'unplugin';
import type { TrackedFilesMatcher } from './plugins/git/trackedFilesMatcher';
-type Assign = Omit & B;
+export type Assign = Omit & B;
export interface RepositoryData {
hash: string;
@@ -88,7 +88,6 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'none';
export type AuthOptions = {
apiKey?: string;
- endPoint?: string;
};
export interface GetPluginsOptions {
diff --git a/packages/plugins/telemetry/README.md b/packages/plugins/telemetry/README.md
index 9f103b3f..8c2ba4ff 100644
--- a/packages/plugins/telemetry/README.md
+++ b/packages/plugins/telemetry/README.md
@@ -15,6 +15,8 @@ Display and send telemetry data as metrics to Datadog.
- [Configuration](#configuration)
- [`disabled`](#disabled)
+ - [`enableTracing`](#enabletracing)
+ - [`endPoint`](#endpoint)
- [`output`](#output)
- [`prefix`](#prefix)
- [`tags`](#tags)
@@ -29,15 +31,14 @@ Display and send telemetry data as metrics to Datadog.
```ts
telemetry?: {
disabled?: boolean;
+ enableTracing?: boolean;
+ endPoint?: string;
output?: boolean
| string
| {
destination: string;
timings?: boolean;
- dependencies?: boolean;
- bundler?: boolean;
metrics?: boolean;
- logs?: boolean;
};
prefix?: string;
tags?: string[];
@@ -52,30 +53,46 @@ telemetry?: {
Plugin will be disabled and won't track anything.
+### `enableTracing`
+
+> default: `false`
+
+> [!NOTE]
+> This option is **deprecated** and will most likely be removed in the future.
+
+If `true`, it will add tracing data to the metrics.
+
+But it is way more time consuming on the build.
+
+And only supports Webpack and Esbuild (for now).
+
+### `endPoint`
+
+> default: `"app.datadoghq.com"`
+
+To which endpoint will the metrics be sent.
+
### `output`
> default: `true`
-If `true`, you'll get all outputs in the logs and the creation of the json files.
-If a path, you'll also save json files at this location:
-
-- `dependencies.json`: track all dependencies and dependents of your modules.
+If `true`, you'll get the creation of both json files:
- `metrics.json`: an array of all the metrics that would be sent to Datadog.
-- `bundler.json`: some 'stats' from your bundler.
- `timings.json`: timing data for modules, loaders and plugins.
+If a path, it will save the files at this location.
+
You can also pass an object of the form:
```javascript
{
destination: 'path/to/destination',
timings: true,
- // This will only output the metrics file and nothing in the logs.
- logs: false,
+ metrics: false,
}
```
-To only output a specified file.
+To only output a specific file.
### `prefix`
@@ -97,7 +114,7 @@ Which timestamp to use when submitting your metrics.
### `filters`
-> default: [`[ filterTreeMetrics, filterSourcemapsAndNodeModules, filterMetricsOnThreshold ]`](../../core/src/helpers.ts)
+> default: [`[ filterTreeMetrics, filterSourcemapsAndNodeModules, filterMetricsOnThreshold ]`](packages/telemetry/src/common/filters.ts)
You can add as many filters as you want. They are just functions getting the `metric` as an argument.
@@ -157,38 +174,40 @@ datadogWebpackPlugin({
## Metrics
> [!CAUTION]
-> Please note that this plugin can generate a lot of metrics, you can greatly reduce their number by tweaking the [`datadog.filters`](./#datadogfilters).
-
-Here's the list of metrics and their corresponding tags:
-
-| Metric | Tags | Type | Description |
-| :---------------------------------- | :----------------------------------------- | :----------- | :----------------------------------------------------- |
-| `${prefix}.assets.count` | `[]` | count | Number of assets. |
-| `${prefix}.assets.size` | `[assetName:${name}, assetType:${type}]` | bytes | Size of an asset file. |
-| `${prefix}.chunks.count` | `[]` | count | Number of chunks. |
-| `${prefix}.chunks.modules.count` | `[chunkName:${name}]` | count | Number of modules in a chunk. |
-| `${prefix}.chunks.size` | `[chunkName:${name}]` | bytes | Size of a chunk. |
-| `${prefix}.compilation.duration` | `[]` | milliseconds | Duration of the build. |
-| `${prefix}.entries.assets.count` | `[entryName:${name}]` | count | Number of assets from an entry. |
-| `${prefix}.entries.chunks.count` | `[entryName:${name}]` | count | Number of chunks from an entry. |
-| `${prefix}.entries.count` | `[]` | count | Number of entries. |
-| `${prefix}.entries.modules.count` | `[entryName:${name}]` | count | Number of modules from an entry. |
-| `${prefix}.entries.size` | `[entryName:${name}]` | bytes | Size of an entry file. |
-| `${prefix}.errors.count` | `[]` | count | Number of errors generated by the build. |
-| `${prefix}.loaders.count` | `[]` | count | Number of loaders. |
-| `${prefix}.loaders.duration` | `[loaderName:${name}]` | milliseconds | Runtime duration of a loader. |
-| `${prefix}.loaders.increment` | `[loaderName:${name}]` | count | Number of hit a loader had. |
-| `${prefix}.modules.count` | `[]` | count | Number of modules. |
-| `${prefix}.modules.dependencies` | `[moduleName:${name}, moduleType:${type}]` | count | Number of dependencies a module has. |
-| `${prefix}.modules.dependents` | `[moduleName:${name}, moduleType:${type}]` | count | Number of dependents a module has. |
-| `${prefix}.modules.size` | `[moduleName:${name}, moduleType:${type}]` | bytes | Size of a module. |
-| `${prefix}.plugins.count` | `[]` | count | Number of plugins. |
-| `${prefix}.plugins.duration` | `[pluginName:${name}]` | milliseconds | Total runtime duration of a plugin. |
-| `${prefix}.plugins.hooks.duration` | `[pluginName:${name}, hookName:${name}]` | milliseconds | Runtime duration of a hook. |
-| `${prefix}.plugins.hooks.increment` | `[pluginName:${name}, hookName:${name}]` | count | Number of hit a hook had. |
-| `${prefix}.plugins.increment` | `[pluginName:${name}]` | count | Number of hit a plugin had. |
-| `${prefix}.plugins.meta.duration` | `[pluginName:datadogwebpackplugin]` | milliseconds | Duration of the process of the Webpack Datadog plugin. |
-| `${prefix}.warnings.count` | `[]` | count | Number of warnings generated by the build. |
+> Please note that this plugin can generate a lot of metrics, you can greatly reduce their number by tweaking the [`datadog.filters`](./#filters).
+
+Here's the list of the metrics that are computed by default:
+
+| Metric | Tags | Type | Description |
+| :---------------------------------- | :------------------------------------------------------------ | :----------- | :----------------------------------------------------- |
+| `${prefix}.assets.count` | `[]` | count | Number of assets. |
+| `${prefix}.assets.size` | `[assetName:${name}, assetType:${type}, entryName:${name}]` | bytes | Size of an asset file. |
+| `${prefix}.assets.modules.count` | `[assetName:${name}, assetType:${type}, entryName:${name}]` | count | Number of modules in a chunk. |
+| `${prefix}.compilation.duration` | `[]` | milliseconds | Duration of the build. |
+| `${prefix}.entries.assets.count` | `[entryName:${name}]` | count | Number of assets from an entry. |
+| `${prefix}.entries.count` | `[]` | count | Number of entries. |
+| `${prefix}.entries.modules.count` | `[entryName:${name}]` | count | Number of modules from an entry. |
+| `${prefix}.entries.size` | `[entryName:${name}]` | bytes | Total size of an entry (and all its assets). |
+| `${prefix}.errors.count` | `[]` | count | Number of errors generated by the build. |
+| `${prefix}.modules.count` | `[]` | count | Number of modules. |
+| `${prefix}.modules.dependencies` | `[moduleName:${name}, moduleType:${type}, assetName:${name}, entryName:${name}]` | count | Number of dependencies a module has. |
+| `${prefix}.modules.dependents` | `[moduleName:${name}, moduleType:${type}, assetName:${name}, entryName:${name}]` | count | Number of dependents a module has. |
+| `${prefix}.modules.size` | `[moduleName:${name}, moduleType:${type}, assetName:${name}, entryName:${name}]` | bytes | Size of a module. |
+| `${prefix}.plugins.meta.duration` | `[pluginName:datadogwebpackplugin]` | milliseconds | Duration of the process of the Webpack Datadog plugin. |
+| `${prefix}.warnings.count` | `[]` | count | Number of warnings generated by the build. |
+
+We also have some metrics that are only available to `esbuild` and `webpack` when the [`enableTracing`](#enableTracing) option is set to `true`:
+
+| Metric | Tags | Type | Description |
+| :---------------------------------- | :--------------------------------------- | :----------- | :---------------------------------- |
+| `${prefix}.loaders.count` | `[]` | count | Number of loaders. |
+| `${prefix}.loaders.duration` | `[loaderName:${name}]` | milliseconds | Runtime duration of a loader. |
+| `${prefix}.loaders.increment` | `[loaderName:${name}]` | count | Number of hit a loader had. |
+| `${prefix}.plugins.count` | `[]` | count | Number of plugins. |
+| `${prefix}.plugins.duration` | `[pluginName:${name}]` | milliseconds | Total runtime duration of a plugin. |
+| `${prefix}.plugins.hooks.duration` | `[pluginName:${name}, hookName:${name}]` | milliseconds | Runtime duration of a hook. |
+| `${prefix}.plugins.hooks.increment` | `[pluginName:${name}, hookName:${name}]` | count | Number of hit a hook had. |
+| `${prefix}.plugins.increment` | `[pluginName:${name}]` | count | Number of hit a plugin had. |
## Dashboard
diff --git a/packages/plugins/telemetry/src/common/aggregator.ts b/packages/plugins/telemetry/src/common/aggregator.ts
index 06435f3a..c2208517 100644
--- a/packages/plugins/telemetry/src/common/aggregator.ts
+++ b/packages/plugins/telemetry/src/common/aggregator.ts
@@ -2,78 +2,208 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import type {
- Report,
- StatsJson,
- BundlerStats,
- EsbuildStats,
- Metric,
- MetricToSend,
- OptionsDD,
-} from '../types';
+import type { Entry, File, GlobalContext, Output } from '@dd/core/types';
+
+import type { Metric, MetricToSend, OptionsDD, Report } from '../types';
import { getMetric } from './helpers';
-import {
- getGenerals,
- getGeneralReport,
- getPlugins,
- getLoaders,
- getDependencies,
-} from './metrics/common';
-import * as es from './metrics/esbuild';
-import * as wp from './metrics/webpack';
-
-const getWebpackMetrics = (statsJson: StatsJson, cwd: string) => {
- const metrics: Metric[] = [];
- const indexed = wp.getIndexed(statsJson, cwd);
- metrics.push(...wp.getModules(statsJson, indexed, cwd));
- metrics.push(...wp.getChunks(statsJson, indexed));
- metrics.push(...wp.getAssets(statsJson, indexed));
- metrics.push(...wp.getEntries(statsJson, indexed));
- return metrics;
+import { getPlugins, getLoaders } from './metrics/common';
+
+const getModuleEntryTags = (file: File, entries: Entry[]) => {
+ const entryNames: string[] = entries
+ .filter((entry) => {
+ const foundModules = entry.inputs.filter((input) => {
+ return input.name === file.name;
+ });
+ return foundModules.length;
+ })
+ .map((entry) => entry.name);
+
+ return Array.from(new Set(entryNames)).map((entryName) => `entryName:${entryName}`);
+};
+
+const getAssetEntryTags = (file: File, entries: Entry[]) => {
+ // Include sourcemaps in the tagging.
+ const cleanAssetName = file.name.replace(/\.map$/, '');
+ const entryNames: string[] = entries
+ .filter((entry) => {
+ const foundModules = entry.outputs.filter((output) => {
+ return output.name === cleanAssetName;
+ });
+ return foundModules.length;
+ })
+ .map((entry) => entry.name);
+
+ return Array.from(new Set(entryNames)).map((entryName) => `entryName:${entryName}`);
+};
+
+const getModuleAssetTags = (file: File, outputs: Output[]) => {
+ const assetNames: string[] = outputs
+ .filter((output) => {
+ return output.inputs.find((input) => input.filepath === file.filepath);
+ })
+ .map((output) => output.name);
+
+ return Array.from(new Set(assetNames)).map((assetName) => `assetName:${assetName}`);
};
-const getEsbuildMetrics = (stats: EsbuildStats, cwd: string) => {
+const getUniversalMetrics = (globalContext: GlobalContext) => {
const metrics: Metric[] = [];
- const indexed = es.getIndexed(stats, cwd);
- metrics.push(...es.getModules(stats, indexed, cwd));
- metrics.push(...es.getAssets(stats, indexed, cwd));
- metrics.push(...es.getEntries(stats, indexed, cwd));
+ const inputs = globalContext.build.inputs || [];
+ const outputs = globalContext.build.outputs || [];
+ const entries = globalContext.build.entries || [];
+ const nbWarnings = globalContext.build.warnings.length;
+ const nbErrors = globalContext.build.errors.length;
+ const duration = globalContext.build.duration;
+
+ // Counts
+ metrics.push(
+ {
+ metric: 'assets.count',
+ type: 'count',
+ value: outputs.length,
+ tags: [],
+ },
+ {
+ metric: 'entries.count',
+ type: 'count',
+ value: entries.length,
+ tags: [],
+ },
+ {
+ metric: 'errors.count',
+ type: 'count',
+ value: nbErrors,
+ tags: [],
+ },
+ {
+ metric: 'modules.count',
+ type: 'count',
+ value: inputs.length,
+ tags: [],
+ },
+ {
+ metric: 'warnings.count',
+ type: 'count',
+ value: nbWarnings,
+ tags: [],
+ },
+ );
+
+ if (duration) {
+ metrics.push({
+ metric: 'compilation.duration',
+ type: 'duration',
+ value: duration,
+ tags: [],
+ });
+ }
+
+ // Modules
+ for (const input of inputs) {
+ const tags = [
+ `moduleName:${input.name}`,
+ `moduleType:${input.type}`,
+ ...getModuleEntryTags(input, entries),
+ ...getModuleAssetTags(input, outputs),
+ ];
+ metrics.push(
+ {
+ metric: 'modules.size',
+ type: 'size',
+ value: input.size,
+ tags,
+ },
+ {
+ metric: 'modules.dependencies',
+ type: 'count',
+ value: input.dependencies.length,
+ tags,
+ },
+ {
+ metric: 'modules.dependents',
+ type: 'count',
+ value: input.dependents.length,
+ tags,
+ },
+ );
+ }
+
+ // Assets
+ for (const output of outputs) {
+ metrics.push(
+ {
+ metric: 'assets.size',
+ type: 'size',
+ value: output.size,
+ tags: [
+ `assetName:${output.name}`,
+ `assetType:${output.type}`,
+ ...getAssetEntryTags(output, entries),
+ ],
+ },
+ {
+ metric: 'assets.modules.count',
+ type: 'count',
+ value: output.inputs.length,
+ tags: [
+ `assetName:${output.name}`,
+ `assetType:${output.type}`,
+ ...getAssetEntryTags(output, entries),
+ ],
+ },
+ );
+ }
+
+ // Entries
+ for (const entry of entries) {
+ const tags = [`entryName:${entry.name}`];
+ metrics.push(
+ {
+ metric: 'entries.size',
+ type: 'size',
+ value: entry.size,
+ tags,
+ },
+ {
+ metric: 'entries.modules.count',
+ type: 'count',
+ value: entry.inputs.length,
+ tags,
+ },
+ {
+ metric: 'entries.assets.count',
+ type: 'count',
+ value: entry.outputs.length,
+ tags,
+ },
+ );
+ }
+
return metrics;
};
export const getMetrics = (
+ globalContext: GlobalContext,
optionsDD: OptionsDD,
- report: Report,
- bundler: BundlerStats,
- cwd: string,
+ report?: Report,
): MetricToSend[] => {
- const { timings, dependencies } = report;
const metrics: Metric[] = [];
- metrics.push(...getGenerals(getGeneralReport(report, bundler)));
+ if (report) {
+ const { timings } = report;
- if (timings) {
- if (timings.tapables) {
- metrics.push(...getPlugins(timings.tapables));
- }
- if (timings.loaders) {
- metrics.push(...getLoaders(timings.loaders));
+ if (timings) {
+ if (timings.tapables) {
+ metrics.push(...getPlugins(timings.tapables));
+ }
+ if (timings.loaders) {
+ metrics.push(...getLoaders(timings.loaders));
+ }
}
}
- if (dependencies) {
- metrics.push(...getDependencies(Object.values(dependencies)));
- }
-
- if (bundler.webpack) {
- const statsJson = bundler.webpack.toJson({ children: false });
- metrics.push(...getWebpackMetrics(statsJson, cwd));
- }
-
- if (bundler.esbuild) {
- metrics.push(...getEsbuildMetrics(bundler.esbuild, cwd));
- }
+ metrics.push(...getUniversalMetrics(globalContext));
// Format metrics to be DD ready and apply filters
const metricsToSend: MetricToSend[] = metrics
diff --git a/packages/plugins/telemetry/src/common/helpers.ts b/packages/plugins/telemetry/src/common/helpers.ts
index 3e480523..836c6fc4 100644
--- a/packages/plugins/telemetry/src/common/helpers.ts
+++ b/packages/plugins/telemetry/src/common/helpers.ts
@@ -14,13 +14,23 @@ import type {
Module,
Compilation,
ValueContext,
+ TelemetryOptionsWithDefaults,
} from '../types';
import { defaultFilters } from './filters';
-export const validateOptions = (options: OptionsWithTelemetry): TelemetryOptions => {
- const validatedOptions: TelemetryOptions = options[CONFIG_KEY] || { disabled: false };
- return validatedOptions;
+export const validateOptions = (opts: OptionsWithTelemetry): TelemetryOptionsWithDefaults => {
+ const options: TelemetryOptions = opts[CONFIG_KEY] || {};
+ return {
+ disabled: false,
+ enableTracing: false,
+ endPoint: 'app.datadoghq.com',
+ filters: defaultFilters,
+ output: false,
+ prefix: '',
+ tags: [],
+ ...options,
+ };
};
export const getMetric = (metric: Metric, opts: OptionsDD): MetricToSend => ({
@@ -30,16 +40,12 @@ export const getMetric = (metric: Metric, opts: OptionsDD): MetricToSend => ({
points: [[opts.timestamp, metric.value]],
});
-export const flattened = (arr: any[]) => [].concat(...arr);
-
-export const getType = (name: string) => (name.includes('.') ? name.split('.').pop() : 'unknown');
-
-export const getOptionsDD = (options: TelemetryOptions): OptionsDD => {
+export const getOptionsDD = (options: TelemetryOptionsWithDefaults): OptionsDD => {
return {
timestamp: Math.floor((options.timestamp || Date.now()) / 1000),
- tags: options.tags || [],
- prefix: options.prefix || '',
- filters: options.filters || defaultFilters,
+ tags: options.tags,
+ prefix: options.prefix,
+ filters: options.filters,
};
};
@@ -109,17 +115,6 @@ export const getModuleName = (module: Module, compilation: Compilation, context?
return formatModuleName(name || 'no-name', context);
};
-export const getModuleSize = (module?: Module): number => {
- if (!module) {
- return 0;
- }
-
- if (typeof module.size === 'function') {
- return module.size();
- }
- return module.size;
-};
-
// Format the loader's name by extracting it from the query.
// "[...]/node_modules/babel-loader/lib/index.js" => babel-loader
const formatLoaderName = (loader: string) =>
diff --git a/packages/plugins/telemetry/src/common/metrics/common.ts b/packages/plugins/telemetry/src/common/metrics/common.ts
index b7a7c960..e7e21bc8 100644
--- a/packages/plugins/telemetry/src/common/metrics/common.ts
+++ b/packages/plugins/telemetry/src/common/metrics/common.ts
@@ -2,88 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import type { Report, LocalModule, TimingsMap, BundlerStats, Metric } from '../../types';
-import { flattened, getType } from '../helpers';
-
-interface GeneralReport {
- modules?: number;
- chunks?: number;
- assets?: number;
- errors?: number;
- warnings?: number;
- entries?: number;
- duration?: number;
-}
-
-export const getGeneralReport = (report: Report, bundler: BundlerStats): GeneralReport => {
- if (bundler.webpack) {
- const stats = bundler.webpack.toJson({ children: false });
- return {
- modules: stats.modules.length,
- chunks: stats.chunks.length,
- assets: stats.assets.length,
- warnings: stats.warnings.length,
- errors: stats.errors.length,
- entries: Object.keys(stats.entrypoints).length,
- duration: stats.time,
- };
- } else if (bundler.esbuild) {
- const stats = bundler.esbuild;
- return {
- modules: report.dependencies ? Object.keys(report.dependencies).length : 0,
- chunks: undefined,
- assets: stats.outputs ? Object.keys(stats.outputs).length : 0,
- warnings: stats.warnings.length,
- errors: stats.errors.length,
- entries: stats.entrypoints ? Object.keys(stats.entrypoints).length : undefined,
- duration: stats.duration,
- };
- }
- return {};
-};
-
-export const getGenerals = (report: GeneralReport): Metric[] => {
- const { duration, ...extracted } = report;
- const metrics: Metric[] = [];
-
- for (const [key, value] of Object.entries(extracted)) {
- metrics.push({
- metric: `${key}.count`,
- type: 'count',
- value,
- tags: [],
- });
- }
-
- if (report.duration) {
- metrics.push({
- metric: 'compilation.duration',
- type: 'duration',
- value: report.duration,
- tags: [],
- });
- }
-
- return metrics;
-};
-
-export const getDependencies = (modules: LocalModule[]): Metric[] =>
- flattened(
- modules.map((m) => [
- {
- metric: 'modules.dependencies',
- type: 'count',
- value: m.dependencies.length,
- tags: [`moduleName:${m.name}`, `moduleType:${getType(m.name)}`],
- },
- {
- metric: 'modules.dependents',
- type: 'count',
- value: m.dependents.length,
- tags: [`moduleName:${m.name}`, `moduleType:${getType(m.name)}`],
- },
- ]),
- );
+import type { TimingsMap, Metric } from '../../types';
export const getPlugins = (plugins: TimingsMap): Metric[] => {
const metrics: Metric[] = [];
diff --git a/packages/plugins/telemetry/src/common/metrics/esbuild.ts b/packages/plugins/telemetry/src/common/metrics/esbuild.ts
deleted file mode 100644
index f11cc1b8..00000000
--- a/packages/plugins/telemetry/src/common/metrics/esbuild.ts
+++ /dev/null
@@ -1,199 +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 { Metafile } from 'esbuild';
-import path from 'path';
-
-import type { EsbuildStats, EsbuildIndexedObject, Metric } from '../../types';
-import { getDisplayName, flattened, getType } from '../helpers';
-
-export const getInputsDependencies = (
- list: Metafile['inputs'],
- moduleName: string,
- deps: Set = new Set(),
-) => {
- const module = list[moduleName];
- for (const dep of module.imports) {
- if (deps.has(dep.path)) {
- continue;
- }
- deps.add(dep.path);
- if (list[dep.path]) {
- getInputsDependencies(list, dep.path, deps);
- }
- }
-
- return deps;
-};
-
-const getModulePath = (fullPath: string, cwd: string): string => {
- const filePath = fullPath.replace('pnp:', '').replace(cwd, '');
- return getDisplayName(path.resolve(cwd, filePath), cwd);
-};
-
-// Get some indexed data to ease the metrics aggregation.
-export const getIndexed = (stats: EsbuildStats, cwd: string): EsbuildIndexedObject => {
- const inputsDependencies: { [key: string]: Set } = {};
- const outputsDependencies: { [key: string]: Set } = {};
-
- const entryNames = new Map();
- if (Array.isArray(stats.entrypoints)) {
- // We don't have an indexed object as entry, so we can't get an entry name from it.
- for (const entry of stats.entrypoints) {
- const fullPath = entry && typeof entry === 'object' ? entry.in : entry;
- const realEntry = getModulePath(fullPath, cwd);
- entryNames.set(realEntry, realEntry);
- }
- } else if (stats.entrypoints) {
- const entrypoints = stats.entrypoints ? Object.entries(stats.entrypoints) : [];
- for (const [entryName, entryPath] of entrypoints) {
- entryNames.set(getModulePath(entryPath, cwd), entryName);
- }
- }
- // First loop to index inputs dependencies.
- const outputs = stats.outputs ? Object.entries(stats.outputs) : [];
- for (const [outputName, output] of outputs) {
- if (output.entryPoint) {
- const entryName = entryNames.get(getDisplayName(output.entryPoint, cwd));
- const inputs = output.inputs ? Object.keys(output.inputs) : [];
- inputsDependencies[entryName] = new Set(inputs);
- outputsDependencies[entryName] = new Set([outputName]);
- for (const input of inputs) {
- if (stats.inputs[input]) {
- const imports = stats.inputs[input].imports.map((imp) => imp.path);
- inputsDependencies[entryName] = new Set([
- ...inputsDependencies[entryName],
- ...imports,
- ]);
- }
- }
- }
- }
-
- // Second loop to index output dependencies.
- // Input dependencies are needed, hence the second loop.
- for (const [outputName, output] of outputs) {
- // Check which entry has generated this output.
- const inputs = output.inputs ? Object.keys(output.inputs) : [];
- for (const inputName of inputs) {
- for (const [entryName, entry] of Object.entries(inputsDependencies)) {
- if (entry.has(inputName)) {
- outputsDependencies[entryName].add(outputName);
- }
- }
- }
- }
-
- return {
- inputsDependencies,
- outputsDependencies,
- entryNames,
- };
-};
-
-export const formatEntryTag = (entryName: string, cwd: string): string => {
- return `entryName:${getDisplayName(entryName, cwd)}`;
-};
-
-export const getEntryTags = (module: string, indexed: EsbuildIndexedObject, cwd: string) => {
- const tags: string[] = [];
- const inputsDependencies = indexed.inputsDependencies
- ? Object.entries(indexed.inputsDependencies)
- : [];
- for (const [entryName, entryDeps] of inputsDependencies) {
- if (entryDeps.has(module)) {
- tags.push(formatEntryTag(entryName, cwd));
- }
- }
- return tags;
-};
-
-export const getModules = (
- stats: EsbuildStats,
- indexed: EsbuildIndexedObject,
- cwd: string,
-): Metric[] => {
- const metrics: Metric[] = [];
-
- const inputs = stats.inputs ? Object.entries(stats.inputs) : [];
- for (const [rawModuleName, module] of inputs) {
- const moduleName = getDisplayName(rawModuleName, cwd);
- const entryTags = getEntryTags(rawModuleName, indexed, cwd);
-
- metrics.push({
- metric: 'module.size',
- type: 'size',
- value: module.bytes,
- tags: [`moduleName:${moduleName}`, `moduleType:${getType(moduleName)}`, ...entryTags],
- });
- }
-
- return metrics;
-};
-
-export const getAssets = (
- stats: EsbuildStats,
- indexed: EsbuildIndexedObject,
- cwd: string,
-): Metric[] => {
- const outputs = stats.outputs ? Object.entries(stats.outputs) : [];
- return outputs.map(([rawAssetName, asset]) => {
- const assetName = getDisplayName(rawAssetName, cwd);
- const entryTags = Array.from(
- new Set(
- flattened(
- Object.keys(asset.inputs).map((modulePath) =>
- getEntryTags(modulePath, indexed, cwd),
- ),
- ),
- ),
- );
-
- return {
- metric: 'assets.size',
- type: 'size',
- value: asset.bytes,
- tags: [`assetName:${assetName}`, `assetType:${getType(assetName)}`, ...entryTags],
- };
- });
-};
-
-export const getEntries = (
- stats: EsbuildStats,
- indexed: EsbuildIndexedObject,
- cwd: string,
-): Metric[] => {
- const metrics: Metric[] = [];
- const outputs = stats.outputs ? Object.entries(stats.outputs) : [];
- for (const [, output] of outputs) {
- if (output.entryPoint) {
- const entryName = indexed.entryNames.get(getModulePath(output.entryPoint, cwd));
- if (entryName) {
- const inputs = getInputsDependencies(stats.inputs, output.entryPoint);
- const tags = [formatEntryTag(entryName, cwd)];
- metrics.push(
- {
- metric: 'entries.size',
- type: 'size',
- value: output.bytes,
- tags,
- },
- {
- metric: 'entries.modules.count',
- type: 'count',
- value: inputs.size,
- tags,
- },
- {
- metric: 'entries.assets.count',
- type: 'count',
- value: indexed.outputsDependencies[entryName].size,
- tags,
- },
- );
- }
- }
- }
- return metrics;
-};
diff --git a/packages/plugins/telemetry/src/common/metrics/webpack.ts b/packages/plugins/telemetry/src/common/metrics/webpack.ts
deleted file mode 100644
index 732c24bb..00000000
--- a/packages/plugins/telemetry/src/common/metrics/webpack.ts
+++ /dev/null
@@ -1,276 +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 { Chunk, StatsJson, Module, Entry, WebpackIndexedObject, Metric } from '../../types';
-import { formatModuleName, getDisplayName, flattened, getType } from '../helpers';
-
-export const getFromId = (coll: any[], id: string) => coll.find((c) => c.id === id);
-
-export const foundInModules = (input: { modules?: Module[] }, identifier?: string): boolean => {
- if (!identifier || !input.modules || !input.modules.length) {
- return false;
- }
-
- return !!input.modules.find((m) => {
- if (m.identifier && m.identifier === identifier) {
- return true;
- // eslint-disable-next-line no-underscore-dangle
- } else if (m._identifier && m._identifier === identifier) {
- return true;
- }
-
- if (m.modules && m.modules.length) {
- return foundInModules(m, identifier);
- }
- });
-};
-
-export const computeEntriesFromChunk = (
- chunk: Chunk,
- indexed: WebpackIndexedObject,
- parentEntries: Set = new Set(),
- parentChunks: Set = new Set(),
-): Set => {
- const entry = indexed.entriesPerChunkId[chunk.id];
-
- if (entry) {
- parentEntries.add(entry.name);
- }
-
- // Escape cyclic dependencies.
- if (parentChunks.has(chunk.id)) {
- return parentEntries;
- }
-
- parentChunks.add(chunk.id);
- chunk.parents.forEach((p: string) => {
- const parentChunk = indexed.chunksPerId[p];
- if (parentChunk) {
- computeEntriesFromChunk(parentChunk, indexed, parentEntries, parentChunks);
- }
- });
- return parentEntries;
-};
-
-export const getEntryTags = (entries: Set): string[] =>
- Array.from(entries).map((e) => `entryName:${e}`);
-
-export const getChunkTags = (chunks: Chunk[]): string[] =>
- flattened(
- chunks
- .map((c) => {
- if (c.names && c.names.length) {
- return c.names.map((n) => `chunkName:${n}`);
- }
- })
- .filter((c) => c),
- );
-
-export const getChunksFromModule = (
- stats: StatsJson,
- chunksPerId: { [key: string]: Chunk },
- module: Module,
-) => {
- if (module.chunks.length) {
- return module.chunks.map((c) => chunksPerId[c]);
- }
-
- // Find the chunks from the chunk list directly.
- // Webpack may not have registered module's chunks in some cases.
- // eslint-disable-next-line no-underscore-dangle
- return stats.chunks.filter((c) => foundInModules(c, module.identifier || module._identifier));
-};
-
-const getMetricsFromModule = (
- stats: StatsJson,
- indexed: WebpackIndexedObject,
- cwd: string,
- module: Module,
-) => {
- const chunks = getChunksFromModule(stats, indexed.chunksPerId, module);
- const entries: Set = new Set();
- for (const chunk of chunks) {
- computeEntriesFromChunk(chunk, indexed, entries);
- }
- const chunkTags = getChunkTags(chunks);
- const entryTags = getEntryTags(entries);
- const moduleName = getDisplayName(module.name, cwd);
-
- return [
- {
- metric: 'modules.size',
- type: 'size',
- value: module.size,
- tags: [
- `moduleName:${moduleName}`,
- `moduleType:${getType(moduleName)}`,
- ...entryTags,
- ...chunkTags,
- ],
- },
- ];
-};
-
-export const getModules = (
- stats: StatsJson,
- indexed: WebpackIndexedObject,
- cwd: string,
-): Metric[] => {
- return flattened(
- Object.values(indexed.modulesPerName).map((module) => {
- return getMetricsFromModule(stats, indexed, cwd, module);
- }),
- );
-};
-
-// Find in entries.chunks
-export const getChunks = (stats: StatsJson, indexed: WebpackIndexedObject): Metric[] => {
- const chunks = stats.chunks;
-
- return flattened(
- chunks.map((chunk) => {
- const entryTags = getEntryTags(computeEntriesFromChunk(chunk, indexed));
- const chunkName = chunk.names.length ? chunk.names.join(' ') : chunk.id;
-
- return [
- {
- metric: 'chunks.size',
- type: 'size',
- value: chunk.size,
- tags: [`chunkName:${chunkName}`, ...entryTags],
- },
- {
- metric: 'chunks.modules.count',
- type: 'count',
- value: chunk.modules.length,
- tags: [`chunkName:${chunkName}`, ...entryTags],
- },
- ];
- }),
- );
-};
-
-export const getAssets = (stats: StatsJson, indexed: WebpackIndexedObject): Metric[] => {
- const assets = stats.assets;
- return assets.map((asset) => {
- const chunks = asset.chunks.map((c) => indexed.chunksPerId[c]);
- const entries: Set = new Set();
- for (const chunk of chunks) {
- computeEntriesFromChunk(chunk, indexed, entries);
- }
- const chunkTags = getChunkTags(chunks);
- const entryTags = getEntryTags(entries);
- const assetName = asset.name;
-
- return {
- metric: 'assets.size',
- type: 'size',
- value: asset.size,
- tags: [
- `assetName:${assetName}`,
- `assetType:${getType(assetName)}`,
- ...chunkTags,
- ...entryTags,
- ],
- };
- });
-};
-
-export const getEntries = (stats: StatsJson, indexed: WebpackIndexedObject): Metric[] =>
- flattened(
- Object.keys(stats.entrypoints).map((entryName) => {
- const entry = stats.entrypoints[entryName];
- const chunks = entry.chunks.map((chunkId) => indexed.chunksPerId[chunkId]!);
-
- let size = 0;
- let moduleCount = 0;
- let assetsCount = 0;
-
- for (const chunk of chunks) {
- size += chunk.size;
- moduleCount += chunk.modules.length;
- assetsCount += chunk.files.length;
- }
-
- return [
- {
- metric: 'entries.size',
- type: 'size',
- value: size,
- tags: [`entryName:${entryName}`],
- },
- {
- metric: 'entries.chunks.count',
- type: 'count',
- value: chunks.length,
- tags: [`entryName:${entryName}`],
- },
- {
- metric: 'entries.modules.count',
- type: 'count',
- value: moduleCount,
- tags: [`entryName:${entryName}`],
- },
- {
- metric: 'entries.assets.count',
- type: 'count',
- value: assetsCount,
- tags: [`entryName:${entryName}`],
- },
- ];
- }),
- );
-
-export const getIndexed = (stats: StatsJson, cwd: string): WebpackIndexedObject => {
- // Gather all modules.
- const modulesPerName: { [key: string]: Module } = {};
- const chunksPerId: { [key: string]: Chunk } = {};
- const entriesPerChunkId: { [key: string]: Entry } = {};
-
- const addModule = (module: Module) => {
- // No internals.
- if (/^webpack\/runtime/.test(module.name)) {
- return;
- }
- // No duplicates.
- if (modulesPerName[formatModuleName(module.name, cwd)]) {
- return;
- }
- // Modules are sometimes registered with their loader.
- if (module.name.includes('!')) {
- return;
- }
-
- modulesPerName[formatModuleName(module.name, cwd)] = module;
- };
-
- for (const [name, entry] of Object.entries(stats.entrypoints)) {
- // In webpack4 we don't have the name of the entry here.
- entry.name = name;
- for (const chunkId of entry.chunks) {
- entriesPerChunkId[chunkId] = entry;
- }
- }
-
- for (const chunk of stats.chunks) {
- chunksPerId[chunk.id] = chunk;
- }
-
- for (const module of stats.modules) {
- // Sometimes modules are grouped together.
- if (module.modules && module.modules.length) {
- for (const moduleIn of module.modules) {
- addModule(moduleIn);
- }
- } else {
- addModule(module);
- }
- }
-
- return {
- modulesPerName,
- chunksPerId,
- entriesPerChunkId,
- };
-};
diff --git a/packages/plugins/telemetry/src/common/modules.ts b/packages/plugins/telemetry/src/common/modules.ts
deleted file mode 100644
index 8c93f3f4..00000000
--- a/packages/plugins/telemetry/src/common/modules.ts
+++ /dev/null
@@ -1,68 +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 { Metafile } from 'esbuild';
-
-import type { LocalModule } from '../types';
-
-import { formatModuleName, getDisplayName } from './helpers';
-
-const modulesMap: { [key: string]: LocalModule } = {};
-
-const getDefaultLocalModule = (name: string): LocalModule => ({
- name: getDisplayName(name),
- chunkNames: [],
- size: 0,
- dependencies: [],
- dependents: [],
-});
-
-export const getModulesResults = (cwd: string, esbuildMeta?: Metafile) => {
- if (!esbuildMeta) {
- return {};
- }
-
- // Indexing chunks so we can access them faster.
- const outputs = esbuildMeta.outputs;
- const chunkIndexed: Record> = {};
- const parseModules = (chunkName: string, moduleName: string) => {
- const formatedModuleName = formatModuleName(moduleName, cwd);
- chunkIndexed[formatedModuleName] = chunkIndexed[formatedModuleName] || new Set();
- const formatedChunkName = chunkName.split('/').pop()?.split('.').shift() || 'unknown';
- chunkIndexed[formatedModuleName].add(formatedChunkName);
-
- if (outputs[moduleName] && outputs[moduleName].inputs.length) {
- for (const inputModuleName of Object.keys(outputs[moduleName].inputs)) {
- parseModules(moduleName, inputModuleName);
- }
- }
- };
-
- for (const [chunkName, chunk] of Object.entries(outputs)) {
- for (const moduleName of Object.keys(chunk.inputs)) {
- parseModules(chunkName, moduleName);
- }
- }
-
- for (const [path, obj] of Object.entries(esbuildMeta.inputs)) {
- const moduleName = formatModuleName(path, cwd);
- const module: LocalModule = modulesMap[moduleName] || getDefaultLocalModule(moduleName);
-
- module.size = obj.bytes;
- if (chunkIndexed[moduleName]) {
- module.chunkNames = Array.from(chunkIndexed[moduleName]);
- }
-
- for (const dependency of obj.imports) {
- const depName = formatModuleName(dependency.path, cwd);
- module.dependencies.push(depName);
- const depObj: LocalModule = modulesMap[depName] || getDefaultLocalModule(depName);
- depObj.dependents.push(moduleName);
- modulesMap[depName] = depObj;
- }
-
- modulesMap[moduleName] = module;
- }
- return modulesMap;
-};
diff --git a/packages/plugins/telemetry/src/common/output/files.ts b/packages/plugins/telemetry/src/common/output/files.ts
index 3bbb74a5..0bcb85f7 100644
--- a/packages/plugins/telemetry/src/common/output/files.ts
+++ b/packages/plugins/telemetry/src/common/output/files.ts
@@ -4,46 +4,45 @@
import { formatDuration } from '@dd/core/helpers';
import type { Logger } from '@dd/core/log';
+import type { MetricToSend, OutputOptions, Report } from '@dd/telemetry-plugins/types';
import path from 'path';
-import type { BundlerContext, OutputOptions } from '../../types';
import { writeFile } from '../helpers';
-type Files = 'timings' | 'dependencies' | 'bundler' | 'metrics';
+type Files = 'timings' | 'metrics';
type FilesToWrite = {
[key in Files]?: { content: any };
};
export const outputFiles = async (
- context: BundlerContext,
+ data: {
+ report?: Report;
+ metrics: MetricToSend[];
+ },
outputOptions: OutputOptions,
log: Logger,
cwd: string,
) => {
- const { report, metrics, bundler } = context;
-
- if (typeof outputOptions !== 'string' && typeof outputOptions !== 'object') {
+ // Don't write any file if it's not enabled.
+ if (typeof outputOptions !== 'string' && typeof outputOptions !== 'object' && !outputOptions) {
return;
}
+ const { report, metrics } = data;
+
const startWriting = Date.now();
- let destination;
+ let destination = '';
const files = {
timings: true,
- dependencies: true,
- bundler: true,
metrics: true,
- result: true,
};
if (typeof outputOptions === 'object') {
destination = outputOptions.destination;
files.timings = outputOptions.timings || false;
- files.dependencies = outputOptions.dependencies || false;
- files.bundler = outputOptions.bundler || false;
files.metrics = outputOptions.metrics || false;
- } else {
+ } else if (typeof outputOptions === 'string') {
destination = outputOptions;
}
@@ -69,19 +68,6 @@ export const outputFiles = async (
};
}
- if (files.dependencies && report?.dependencies) {
- filesToWrite.dependencies = { content: report.dependencies };
- }
-
- if (files.bundler) {
- if (bundler.webpack) {
- filesToWrite.bundler = { content: bundler.webpack.toJson({ children: false }) };
- }
- if (bundler.esbuild) {
- filesToWrite.bundler = { content: bundler.esbuild };
- }
- }
-
if (metrics && files.metrics) {
filesToWrite.metrics = { content: metrics };
}
diff --git a/packages/plugins/telemetry/src/common/output/index.ts b/packages/plugins/telemetry/src/common/output/index.ts
deleted file mode 100644
index b8ff7e07..00000000
--- a/packages/plugins/telemetry/src/common/output/index.ts
+++ /dev/null
@@ -1,29 +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 { Logger } from '@dd/core/log';
-import type { GlobalContext } from '@dd/core/types';
-
-import type { BundlerContext, TelemetryOptions } from '../../types';
-import { getOptionsDD } from '../helpers';
-
-import { outputFiles } from './files';
-import { addMetrics, processMetrics } from './metrics';
-import { outputTexts } from './text';
-
-export const output = async (
- bundlerContext: BundlerContext,
- options: TelemetryOptions,
- logger: Logger,
- context: GlobalContext,
-) => {
- const outputOptions = options.output;
- const optionsDD = getOptionsDD(options);
-
- addMetrics(bundlerContext, optionsDD, logger, context.cwd);
- outputTexts(bundlerContext, outputOptions || true, context);
- // TODO Handle defaults earlier (outputOptions || true).
- await outputFiles(bundlerContext, outputOptions || true, logger, context.cwd);
- await processMetrics(bundlerContext, optionsDD, logger);
-};
diff --git a/packages/plugins/telemetry/src/common/output/metrics.ts b/packages/plugins/telemetry/src/common/output/metrics.ts
deleted file mode 100644
index 041e6ed2..00000000
--- a/packages/plugins/telemetry/src/common/output/metrics.ts
+++ /dev/null
@@ -1,53 +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 { formatDuration } from '@dd/core/helpers';
-import type { Logger } from '@dd/core/log';
-
-import { PLUGIN_NAME } from '../../constants';
-import type { BundlerContext, OptionsDD } from '../../types';
-import { getMetrics } from '../aggregator';
-import { getMetric } from '../helpers';
-
-export const addMetrics = (
- bundlerContext: BundlerContext,
- optionsDD: OptionsDD,
- log: Logger,
- cwd: string,
-) => {
- const { report, bundler } = bundlerContext;
-
- bundlerContext.metrics = bundlerContext.metrics || [];
- try {
- bundlerContext.metrics = getMetrics(optionsDD, report, bundler, cwd);
- } catch (e) {
- const stack = e instanceof Error ? e.stack : e;
- log(`Couldn't aggregate metrics: ${stack}`, 'error');
- }
-};
-
-export const processMetrics = async (
- bundlerContext: BundlerContext,
- optionsDD: OptionsDD,
- log: Logger,
-) => {
- const { start } = bundlerContext;
- const duration = Date.now() - start;
-
- bundlerContext.metrics = bundlerContext.metrics || [];
- // We're missing the duration of this hook for our plugin.
- bundlerContext.metrics.push(
- getMetric(
- {
- tags: [`pluginName:${PLUGIN_NAME}`],
- metric: `plugins.meta.duration`,
- type: 'duration',
- value: duration,
- },
- optionsDD,
- ),
- );
-
- log(`Took ${formatDuration(duration)}.`);
-};
diff --git a/packages/plugins/telemetry/src/common/output/text.ts b/packages/plugins/telemetry/src/common/output/text.ts
index bcc8bbc6..5cae5b48 100644
--- a/packages/plugins/telemetry/src/common/output/text.ts
+++ b/packages/plugins/telemetry/src/common/output/text.ts
@@ -3,26 +3,30 @@
// Copyright 2019-Present Datadog, Inc.
import { formatDuration } from '@dd/core/helpers';
-import type { GlobalContext } from '@dd/core/types';
+import type { Logger } from '@dd/core/log';
+import type { Entry, GlobalContext, Output } from '@dd/core/types';
import chalk from 'chalk';
import prettyBytes from 'pretty-bytes';
-import type {
- BundlerContext,
- EsbuildStats,
- LocalModule,
- LocalModules,
- OutputOptions,
- Stats,
- TimingsMap,
-} from '../../types';
+import type { Report, TimingsMap } from '../../types';
+// How many items do we show in the top lists.
const TOP = 5;
const numColor = chalk.bold.red;
const nameColor = chalk.bold.cyan;
+type ValuesToPrint = { name: string; top: boolean; values: { name: string; value: string }[] };
+
+type FileReport = {
+ name: string;
+ aggregatedSize?: number;
+ size: number;
+ dependencies: string[];
+ dependents: string[];
+};
+
// Sort a collection by attribute
-const sortDesc = (attr: any) => (a: any, b: any) => {
+const sortDesc = (attr: ((arg: any) => any) | string) => (a: any, b: any) => {
let aVal;
let bVal;
@@ -43,224 +47,297 @@ const sortDesc = (attr: any) => (a: any, b: any) => {
}
};
-const getOutput = (values: any[], renderValue: (arg: any) => string): string => {
- let output = '';
- for (const val of values.slice(0, TOP)) {
- output += `[${numColor(renderValue(val))}] ${nameColor(val.name)}\n`;
+export const getGeneralValues = (context: GlobalContext): ValuesToPrint[] => {
+ const valuesToPrint: ValuesToPrint = {
+ name: 'General Numbers',
+ values: [],
+ top: false,
+ };
+
+ const nbModules = context.build.inputs ? context.build.inputs.length : 0;
+ const nbAssets = context.build.outputs ? context.build.outputs.length : 0;
+ const nbWarnings = context.build.warnings.length;
+ const nbErrors = context.build.errors.length;
+ const nbEntries = context.build.entries ? context.build.entries.length : 0;
+
+ if (context.build.start) {
+ valuesToPrint.values.push({
+ name: 'Overhead duration',
+ value: formatDuration(context.build.start - context.start),
+ });
}
- return output;
-};
-
-const outputTapables = (timings?: TimingsMap): string => {
- let output = '';
- if (!timings) {
- return output;
+ if (context.build.duration) {
+ valuesToPrint.values.push({
+ name: 'Build duration',
+ value: formatDuration(context.build.duration),
+ });
}
- const times = Array.from(timings.values());
-
- if (!times.length) {
- return output;
+ if (context.build.writeDuration) {
+ valuesToPrint.values.push({
+ name: 'Write duration',
+ value: formatDuration(context.build.writeDuration),
+ });
}
- // Output
- output += '\n===== Tapables =====\n';
- output += `\n=== Top ${TOP} duration ===\n`;
- // Sort by time, longest first
- times.sort(sortDesc('duration'));
- output += getOutput(times, (time) => formatDuration(time.duration));
- output += `\n=== Top ${TOP} hits ===\n`;
- // Sort by time, longest first
- times.sort(sortDesc('increment'));
- output += getOutput(times, (plugin) => plugin.increment);
-
- return output;
+ valuesToPrint.values.push(
+ {
+ name: 'Number of modules',
+ value: nbModules.toString(),
+ },
+ {
+ name: 'Number of assets',
+ value: nbAssets.toString(),
+ },
+ {
+ name: 'Number of entries',
+ value: nbEntries.toString(),
+ },
+ {
+ name: 'Number of warnings',
+ value: nbWarnings.toString(),
+ },
+ {
+ name: 'Number of errors',
+ value: nbErrors.toString(),
+ },
+ );
+
+ return [valuesToPrint];
};
-export const outputWebpack = (stats: Stats): string => {
- let output = '\n===== General =====\n';
- // More general stuffs.
- const duration = stats.endTime - stats.startTime;
- const nbDeps = stats.compilation.fileDependencies.size;
- // In Webpack 5, stats.compilation.emittedAssets doesn't exist.
- const nbFiles = stats.compilation.assets
- ? Object.keys(stats.compilation.assets).length
- : stats.compilation.emittedAssets.size;
- const nbWarnings = stats.compilation.warnings.length;
- // In Webpack 5, stats.compilation.modules is a Set.
- const nbModules =
- 'size' in stats.compilation.modules
- ? stats.compilation.modules.size
- : stats.compilation.modules.length;
- // In Webpack 5, stats.compilation.chunks is a Set.
- const nbChunks =
- 'size' in stats.compilation.chunks
- ? stats.compilation.chunks.size
- : stats.compilation.chunks.length;
- // In Webpack 5, stats.compilation.entries is a Map.
- const nbEntries =
- 'size' in stats.compilation.entries
- ? stats.compilation.entries.size
- : stats.compilation.entries.length;
- output += `duration: ${chalk.bold(formatDuration(duration))}
-nbDeps: ${chalk.bold(nbDeps.toString())}
-nbFiles: ${chalk.bold(nbFiles.toString())}
-nbWarnings: ${chalk.bold(nbWarnings.toString())}
-nbModules: ${chalk.bold(nbModules.toString())}
-nbChunks: ${chalk.bold(nbChunks.toString())}
-nbEntries: ${chalk.bold(nbEntries.toString())}
-`;
- return output;
+const getAssetsValues = (context: GlobalContext): ValuesToPrint[] => {
+ const assetSizesToPrint: ValuesToPrint = {
+ name: 'Asset size',
+ values: (context.build.outputs || [])
+ .sort(sortDesc((output: Output) => output.size))
+ .map((output) => ({
+ name: output.name,
+ value: prettyBytes(output.size),
+ })),
+ top: true,
+ };
+
+ const entrySizesToPrint: ValuesToPrint = {
+ name: 'Entry aggregated size',
+ values: (context.build.entries || [])
+ .sort(sortDesc((entry: Entry) => entry.size))
+ .map((entry) => ({
+ name: entry.name,
+ value: prettyBytes(entry.size),
+ })),
+ top: true,
+ };
+
+ const entryModulesToPrint: ValuesToPrint = {
+ name: 'Entry number of modules',
+ values:
+ (context.build.entries || [])
+ .sort(sortDesc((entry: Entry) => entry.size))
+ .map((entry) => ({
+ name: entry.name,
+ value: entry.inputs.length.toString(),
+ })) || [],
+ top: true,
+ };
+
+ return [assetSizesToPrint, entrySizesToPrint, entryModulesToPrint];
};
-export const outputEsbuild = (stats: EsbuildStats) => {
- let output = '\n===== General =====\n';
- const nbDeps = stats.inputs ? Object.keys(stats.inputs).length : 0;
- const nbFiles = stats.outputs ? Object.keys(stats.outputs).length : 0;
- const nbWarnings = stats.warnings.length;
- const nbErrors = stats.errors.length;
- const nbEntries = stats.entrypoints ? Object.keys(stats.entrypoints).length : 0;
-
- output += `
-nbDeps: ${chalk.bold(nbDeps.toString())}
-nbFiles: ${chalk.bold(nbFiles.toString())}
-nbWarnings: ${chalk.bold(nbWarnings.toString())}
-nbErrors: ${chalk.bold(nbErrors.toString())}
-nbEntries: ${chalk.bold(nbEntries.toString())}
-`;
- return output;
-};
-
-const outputLoaders = (timings?: TimingsMap): string => {
- let output = '';
-
- if (!timings) {
- return output;
- }
-
- const times = Array.from(timings.values());
-
- if (!times.length) {
- return output;
+// Crawl through collection to gather all dependencies or dependents.
+const getAll = (
+ attribute: 'dependents' | 'dependencies',
+ collection: Record,
+ filepath: string,
+ accumulator: string[] = [],
+): string[] => {
+ const reported: string[] = collection[filepath]?.[attribute] || [];
+ for (const reportedFilename of reported) {
+ if (accumulator.includes(reportedFilename) || reportedFilename === filepath) {
+ continue;
+ }
+
+ accumulator.push(reportedFilename);
+ getAll(attribute, collection, reportedFilename, accumulator);
}
-
- // Output
- output += '\n===== Loaders =====\n';
- output += `\n=== Top ${TOP} duration ===\n`;
- // Sort by time, longest first
- times.sort(sortDesc('duration'));
- output += getOutput(times, (loader) => formatDuration(loader.duration));
- output += `\n=== Top ${TOP} hits ===\n`;
- // Sort by hits, biggest first
- times.sort(sortDesc('increment'));
- output += getOutput(times, (loader) => loader.increment);
-
- return output;
+ return accumulator;
};
-const outputModulesDependencies = (deps: LocalModules): string => {
- let output = '';
-
- if (!deps) {
- return output;
+const getModulesValues = (context: GlobalContext): ValuesToPrint[] => {
+ const dependentsToPrint: ValuesToPrint = {
+ name: `Module total dependents`,
+ values: [],
+ top: true,
+ };
+
+ const dependenciesToPrint: ValuesToPrint = {
+ name: `Module total dependencies`,
+ values: [],
+ top: true,
+ };
+
+ const sizesToPrint: ValuesToPrint = {
+ name: `Module size`,
+ values: [],
+ top: true,
+ };
+
+ const aggregatedSizesToPrint: ValuesToPrint = {
+ name: `Module aggregated size`,
+ values: [],
+ top: true,
+ };
+
+ const dependencies: FileReport[] = [];
+
+ // Build our collections.
+ const inputs = Object.fromEntries(
+ (context.build.inputs || []).map((input) => [
+ input.filepath,
+ {
+ name: input.name,
+ size: input.size,
+ dependencies: input.dependencies.map((dep) => dep.filepath),
+ dependents: input.dependents.map((dep) => dep.filepath),
+ },
+ ]),
+ );
+
+ for (const filepath in inputs) {
+ if (!Object.hasOwn(inputs, filepath)) {
+ continue;
+ }
+
+ const fileDependencies = getAll('dependencies', inputs, filepath);
+ // Aggregate size.
+ const aggregatedSize = fileDependencies.reduce(
+ (acc, dep) => acc + inputs[dep].size,
+ inputs[filepath].size,
+ );
+
+ dependencies.push({
+ name: inputs[filepath].name,
+ size: inputs[filepath].size,
+ aggregatedSize,
+ dependents: getAll('dependents', inputs, filepath),
+ dependencies: fileDependencies,
+ });
}
- const dependencies = Object.values(deps);
-
if (!dependencies.length) {
- return output;
+ return [dependentsToPrint, dependenciesToPrint, sizesToPrint];
}
- output += '\n===== Modules =====\n';
// Sort by dependents, biggest first
- dependencies.sort(sortDesc((mod: LocalModule) => mod.dependents.length));
- output += `\n=== Top ${TOP} dependents ===\n`;
- output += getOutput(dependencies, (module) => module.dependents.length);
+ dependencies.sort(sortDesc((file: FileReport) => file.dependents.length));
+ dependentsToPrint.values = dependencies.map((file) => ({
+ name: file.name,
+ value: file.dependents.length.toString(),
+ }));
// Sort by dependencies, biggest first
- dependencies.sort(sortDesc((mod: LocalModule) => mod.dependencies.length));
- output += `\n=== Top ${TOP} dependencies ===\n`;
- output += getOutput(dependencies, (module) => module.dependencies.length);
+ dependencies.sort(sortDesc((file: FileReport) => file.dependencies.length));
+ dependenciesToPrint.values = dependencies.map((file) => ({
+ name: file.name,
+ value: file.dependencies.length.toString(),
+ }));
// Sort by size, biggest first
dependencies.sort(sortDesc('size'));
- output += `\n=== Top ${TOP} size ===\n`;
- output += getOutput(dependencies, (module) => prettyBytes(module.size));
-
- return output;
+ sizesToPrint.values = dependencies.map((file) => ({
+ name: file.name,
+ value: prettyBytes(file.size),
+ }));
+ // Sort by aggregated size, biggest first
+ dependencies.sort(sortDesc('aggregatedSize'));
+ aggregatedSizesToPrint.values = dependencies.map((file) => ({
+ name: file.name,
+ value: prettyBytes(file.aggregatedSize || file.size),
+ }));
+
+ return [dependentsToPrint, dependenciesToPrint, sizesToPrint, aggregatedSizesToPrint];
};
-const outputModulesTimings = (timings?: TimingsMap): string => {
- let output = '';
-
- if (!timings) {
- return output;
+const getTimingValues = (name: string, timings?: TimingsMap): ValuesToPrint[] => {
+ if (!timings || !timings.size) {
+ return [];
}
const times = Array.from(timings.values());
-
- if (!times.length) {
- return output;
- }
-
- output += '\n===== Modules =====\n';
// Sort by duration, longest first
times.sort(sortDesc('duration'));
- output += `\n=== Top ${TOP} duration ===\n`;
- output += getOutput(times, (module) => formatDuration(module.duration));
- // Sort by increment, longest first
+ const durationsToPrint: ValuesToPrint = {
+ name: `${name} duration`,
+ values: times.map((module) => ({
+ name: module.name,
+ value: formatDuration(module.duration),
+ })),
+ top: true,
+ };
+
+ // Sort by increment, biggest first
times.sort(sortDesc('increment'));
- output += `\n=== Top ${TOP} hits ===\n`;
- output += getOutput(times, (module) => module.increment);
-
- return output;
+ const hitsToPrint: ValuesToPrint = {
+ name: `${name} hits`,
+ values: times.map((module) => ({
+ name: module.name,
+ value: module.increment.toString(),
+ })),
+ top: true,
+ };
+
+ return [durationsToPrint, hitsToPrint];
};
-const shouldShowOutput = (output?: OutputOptions): boolean => {
- if (typeof output === 'boolean') {
- return output;
- }
-
- // If we passed a path, we should output as stated in the docs.
- if (typeof output === 'string') {
- return true;
- }
-
- // If we passed nothing, default is true.
- if (!output) {
- return true;
+const renderValues = (values: ValuesToPrint[]): string => {
+ let outputString = '';
+ const titlePadding = 4;
+ const valuePadding = 4;
+ const maxTitleWidth = Math.max(...values.map((val) => val.name.length));
+ const maxNameWidth = Math.max(...values.flatMap((val) => val.values.map((v) => v.name.length)));
+ const maxValueWidth = Math.max(
+ ...values.flatMap((val) => val.values.map((v) => v.value.length)),
+ );
+ const totalWidth = Math.max(
+ maxTitleWidth + titlePadding,
+ maxNameWidth + maxValueWidth + valuePadding,
+ );
+
+ for (const group of values) {
+ if (group.values.length === 0) {
+ continue;
+ }
+
+ const title =
+ group.top && group.values.length >= TOP ? `Top ${TOP} ${group.name}` : group.name;
+ const titlePad = totalWidth - (title.length + titlePadding);
+
+ outputString += `\n== ${title} ${'='.repeat(titlePad)}=\n`;
+
+ const valuesToPrint = group.top ? group.values.slice(0, TOP) : group.values;
+ for (const value of valuesToPrint) {
+ const valuePad = maxValueWidth - value.value.length;
+ outputString += ` [${numColor(value.value)}] ${' '.repeat(valuePad)}${nameColor(value.name)}\n`;
+ }
}
- // Finally, if we passed an object, we should check if logs are enabled.
- return output.logs !== false;
+ return outputString;
};
-export const outputTexts = (
- bundlerContext: BundlerContext,
- output: OutputOptions,
- context: GlobalContext,
-) => {
- const { report, bundler } = bundlerContext;
+export const outputTexts = (globalContext: GlobalContext, log: Logger, report?: Report) => {
+ const valuesToPrint: ValuesToPrint[] = [];
- if (!shouldShowOutput(output)) {
- return;
+ if (report) {
+ // Output legacy/tracing.
+ valuesToPrint.push(...getTimingValues('Loader', report.timings.loaders));
+ valuesToPrint.push(...getTimingValues('Tapable', report.timings.tapables));
+ valuesToPrint.push(...getTimingValues('Module', report.timings.modules));
}
- let outputString = '';
+ valuesToPrint.push(...getModulesValues(globalContext));
+ valuesToPrint.push(...getAssetsValues(globalContext));
+ valuesToPrint.push(...getGeneralValues(globalContext));
- if (report) {
- outputString += outputTapables(report.timings.tapables);
- outputString += outputLoaders(report.timings.loaders);
- outputString += outputModulesDependencies(report.dependencies);
- outputString += outputModulesTimings(report.timings.modules);
- }
- if (bundler.webpack) {
- outputString += outputWebpack(bundler.webpack);
- }
- if (bundler.esbuild) {
- outputString += outputEsbuild(bundler.esbuild);
- }
+ const outputString = renderValues(valuesToPrint);
- // We're using console.log here because the configuration expressely asked us to print it.
- // eslint-disable-next-line no-console
- console.log(outputString);
+ log(outputString, 'info');
};
diff --git a/packages/plugins/telemetry/src/common/sender.ts b/packages/plugins/telemetry/src/common/sender.ts
index a5c026c3..dd5736b7 100644
--- a/packages/plugins/telemetry/src/common/sender.ts
+++ b/packages/plugins/telemetry/src/common/sender.ts
@@ -7,15 +7,15 @@ import type { Logger } from '@dd/core/log';
import { request } from 'https';
import type { ServerResponse } from 'http';
-import type { MetricToSend, OptionsWithTelemetry } from '../types';
+import type { MetricToSend } from '../types';
export const sendMetrics = (
metrics: MetricToSend[] | undefined,
- opts: OptionsWithTelemetry,
+ auth: { apiKey?: string; endPoint: string },
log: Logger,
) => {
const startSending = Date.now();
- if (!opts.auth?.apiKey) {
+ if (!auth.apiKey) {
log(`Won't send metrics to Datadog: missing API Key.`, 'warn');
return;
}
@@ -37,8 +37,8 @@ Metrics:
return new Promise((resolve, reject) => {
const req = request({
method: 'POST',
- hostname: opts.auth?.endPoint || 'app.datadoghq.com',
- path: `/api/v1/series?api_key=${opts.auth?.apiKey}`,
+ hostname: auth.endPoint,
+ path: `/api/v1/series?api_key=${auth.apiKey}`,
});
req.write(
diff --git a/packages/plugins/telemetry/src/esbuild-plugin/index.ts b/packages/plugins/telemetry/src/esbuild-plugin/index.ts
index e378466c..7caaa818 100644
--- a/packages/plugins/telemetry/src/esbuild-plugin/index.ts
+++ b/packages/plugins/telemetry/src/esbuild-plugin/index.ts
@@ -2,59 +2,41 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import { getLogger } from '@dd/core/log';
+import type { Logger } from '@dd/core/log';
import type { GlobalContext } from '@dd/core/types';
import type { BuildResult } from 'esbuild';
import type { UnpluginOptions } from 'unplugin';
-import { validateOptions } from '../common/helpers';
-import { output } from '../common/output';
-import { sendMetrics } from '../common/sender';
-import type { BundlerContext, OptionsWithTelemetry } from '../types';
+import type { BundlerContext } from '../types';
-import { getModulesResults } from './modules';
import { wrapPlugins, getResults as getPluginsResults } from './plugins';
export const getEsbuildPlugin = (
- opt: OptionsWithTelemetry,
- ctx: GlobalContext,
+ bundlerContext: BundlerContext,
+ globalContext: GlobalContext,
+ logger: Logger,
): UnpluginOptions['esbuild'] => {
return {
setup: (build) => {
- const startBuild = Date.now();
- const logger = getLogger(opt.logLevel, 'telemetry');
- const telemetryOptions = validateOptions(opt);
+ globalContext.build.start = Date.now();
+
// We force esbuild to produce its metafile.
build.initialOptions.metafile = true;
- wrapPlugins(build, ctx.cwd);
+ wrapPlugins(build, globalContext.cwd);
build.onEnd(async (result: BuildResult) => {
+ if (!result.metafile) {
+ logger("Missing metafile, can't proceed with modules data.", 'warn');
+ return;
+ }
+
const { plugins, modules } = getPluginsResults();
- // We know it exists since we're setting the option earlier.
- const metaFile = result.metafile!;
- const moduleResults = getModulesResults(ctx.cwd, metaFile);
- const bundlerContext: BundlerContext = {
- start: startBuild,
- report: {
- timings: {
- tapables: plugins,
- modules,
- },
- dependencies: moduleResults,
- },
- bundler: {
- esbuild: {
- warnings: result.warnings,
- errors: result.errors,
- entrypoints: build.initialOptions.entryPoints,
- duration: Date.now() - startBuild,
- ...metaFile,
- },
+ bundlerContext.report = {
+ timings: {
+ tapables: plugins,
+ modules,
},
};
-
- await output(bundlerContext, telemetryOptions, logger, ctx);
- await sendMetrics(bundlerContext.metrics, opt, logger);
});
},
};
diff --git a/packages/plugins/telemetry/src/esbuild-plugin/modules.ts b/packages/plugins/telemetry/src/esbuild-plugin/modules.ts
deleted file mode 100644
index 08b50b61..00000000
--- a/packages/plugins/telemetry/src/esbuild-plugin/modules.ts
+++ /dev/null
@@ -1,67 +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 { Metafile } from 'esbuild';
-
-import { formatModuleName, getDisplayName } from '../common/helpers';
-import type { LocalModule } from '../types';
-
-const modulesMap: { [key: string]: LocalModule } = {};
-
-const getDefaultLocalModule = (name: string): LocalModule => ({
- name: getDisplayName(name),
- chunkNames: [],
- size: 0,
- dependencies: [],
- dependents: [],
-});
-
-export const getModulesResults = (cwd: string, esbuildMeta?: Metafile) => {
- if (!esbuildMeta) {
- return {};
- }
-
- // Indexing chunks so we can access them faster.
- const outputs = esbuildMeta.outputs;
- const chunkIndexed: Record> = {};
- const parseModules = (chunkName: string, moduleName: string) => {
- const formatedModuleName = formatModuleName(moduleName, cwd);
- chunkIndexed[formatedModuleName] = chunkIndexed[formatedModuleName] || new Set();
- const formatedChunkName = chunkName.split('/').pop()?.split('.').shift() || 'unknown';
- chunkIndexed[formatedModuleName].add(formatedChunkName);
-
- if (outputs[moduleName] && outputs[moduleName].inputs.length) {
- for (const inputModuleName of Object.keys(outputs[moduleName].inputs)) {
- parseModules(moduleName, inputModuleName);
- }
- }
- };
-
- for (const [chunkName, chunk] of Object.entries(outputs)) {
- for (const moduleName of Object.keys(chunk.inputs)) {
- parseModules(chunkName, moduleName);
- }
- }
-
- for (const [path, obj] of Object.entries(esbuildMeta.inputs)) {
- const moduleName = formatModuleName(path, cwd);
- const module: LocalModule = modulesMap[moduleName] || getDefaultLocalModule(moduleName);
-
- module.size = obj.bytes;
- if (chunkIndexed[moduleName]) {
- module.chunkNames = Array.from(chunkIndexed[moduleName]);
- }
-
- for (const dependency of obj.imports) {
- const depName = formatModuleName(dependency.path, cwd);
- module.dependencies.push(depName);
- const depObj: LocalModule = modulesMap[depName] || getDefaultLocalModule(depName);
- depObj.dependents.push(moduleName);
- modulesMap[depName] = depObj;
- }
-
- modulesMap[moduleName] = module;
- }
- return modulesMap;
-};
diff --git a/packages/plugins/telemetry/src/esbuild-plugin/plugins.ts b/packages/plugins/telemetry/src/esbuild-plugin/plugins.ts
index 35ca7b6b..49ea49f4 100644
--- a/packages/plugins/telemetry/src/esbuild-plugin/plugins.ts
+++ b/packages/plugins/telemetry/src/esbuild-plugin/plugins.ts
@@ -25,7 +25,7 @@ export const wrapPlugins = (build: PluginBuild, context: string) => {
});
for (const plugin of plugins) {
// Skip the current plugin.
- if (plugin.name === PLUGIN_NAME) {
+ if (plugin.name.includes(PLUGIN_NAME)) {
continue;
}
diff --git a/packages/plugins/telemetry/src/index.ts b/packages/plugins/telemetry/src/index.ts
index 43731d8b..e059e51b 100644
--- a/packages/plugins/telemetry/src/index.ts
+++ b/packages/plugins/telemetry/src/index.ts
@@ -2,12 +2,24 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import type { GlobalContext, GetPlugins } from '@dd/core/types';
+import { getLogger } from '@dd/core/log';
+import type { GlobalContext, GetPlugins, PluginOptions } from '@dd/core/types';
+import { getMetrics } from './common/aggregator';
import { defaultFilters } from './common/filters';
+import { getOptionsDD, validateOptions } from './common/helpers';
+import { outputFiles } from './common/output/files';
+import { outputTexts } from './common/output/text';
+import { sendMetrics } from './common/sender';
import { PLUGIN_NAME, CONFIG_KEY } from './constants';
import { getEsbuildPlugin } from './esbuild-plugin';
-import type { Filter, Metric, OptionsWithTelemetry, TelemetryOptions } from './types';
+import type {
+ BundlerContext,
+ Filter,
+ Metric,
+ OptionsWithTelemetry,
+ TelemetryOptions,
+} from './types';
import { getWebpackPlugin } from './webpack-plugin';
export { CONFIG_KEY, PLUGIN_NAME };
@@ -26,11 +38,67 @@ export const getPlugins: GetPlugins = (
options: OptionsWithTelemetry,
context: GlobalContext,
) => {
- return [
- {
- name: PLUGIN_NAME,
- esbuild: getEsbuildPlugin(options, context),
- webpack: getWebpackPlugin(options, context),
+ let realBuildEnd: number = 0;
+ const bundlerContext: BundlerContext = {
+ start: Date.now(),
+ };
+
+ const telemetryOptions = validateOptions(options);
+ const logger = getLogger(options.logLevel, PLUGIN_NAME);
+ const plugins: PluginOptions[] = [];
+
+ // Webpack and Esbuild specific plugins.
+ // LEGACY
+ const legacyPlugin: PluginOptions = {
+ name: PLUGIN_NAME,
+ enforce: 'pre',
+ esbuild: getEsbuildPlugin(bundlerContext, context, logger),
+ webpack: getWebpackPlugin(bundlerContext, context),
+ };
+ // Universal plugin.
+ const universalPlugin: PluginOptions = {
+ name: 'datadog-universal-telemetry-plugin',
+ enforce: 'post',
+ buildStart() {
+ context.build.start = context.build.start || Date.now();
+ },
+ buildEnd() {
+ realBuildEnd = Date.now();
+ },
+
+ // Move as much as possible in the universal plugin.
+ async writeBundle() {
+ context.build.end = Date.now();
+ context.build.duration = context.build.end - context.build.start!;
+ context.build.writeDuration = context.build.end - realBuildEnd;
+
+ const metrics = [];
+ const optionsDD = getOptionsDD(telemetryOptions);
+
+ metrics.push(...getMetrics(context, optionsDD, bundlerContext.report));
+
+ // TODO Extract the files output in an internal plugin.
+ await outputFiles(
+ { report: bundlerContext.report, metrics },
+ telemetryOptions.output,
+ logger,
+ context.bundler.outDir,
+ );
+ outputTexts(context, logger, bundlerContext.report);
+
+ await sendMetrics(
+ metrics,
+ { apiKey: context.auth?.apiKey, endPoint: telemetryOptions.endPoint },
+ logger,
+ );
},
- ];
+ };
+
+ if (telemetryOptions.enableTracing) {
+ plugins.push(legacyPlugin);
+ }
+
+ plugins.push(universalPlugin);
+
+ return plugins;
};
diff --git a/packages/plugins/telemetry/src/types.ts b/packages/plugins/telemetry/src/types.ts
index e9e0be94..9ce35a77 100644
--- a/packages/plugins/telemetry/src/types.ts
+++ b/packages/plugins/telemetry/src/types.ts
@@ -2,8 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import type { GetPluginsOptions } from '@dd/core/types';
-import type { Metafile, Message, BuildOptions, BuildResult } from 'esbuild';
+import type { Assign, GetPluginsOptions } from '@dd/core/types';
import type { CONFIG_KEY } from './constants';
@@ -36,54 +35,47 @@ export type OutputOptions =
| {
destination: string;
timings?: boolean;
- dependencies?: boolean;
- bundler?: boolean;
metrics?: boolean;
- logs?: boolean;
};
export type TelemetryOptions = {
disabled?: boolean;
+ /** @deprecated */
+ enableTracing?: boolean;
+ endPoint?: string;
+ filters?: Filter[];
output?: OutputOptions;
prefix?: string;
tags?: string[];
timestamp?: number;
- filters?: Filter[];
};
+export type TelemetryOptionsWithDefaults = Assign<
+ Required,
+ {
+ timestamp?: TelemetryOptions['timestamp'];
+ }
+>;
+
export interface OptionsWithTelemetry extends GetPluginsOptions {
[CONFIG_KEY]: TelemetryOptions;
}
-interface EsbuildBundlerResult extends Metafile {
- warnings: BuildResult['warnings'];
- errors: BuildResult['errors'];
- entrypoints: BuildOptions['entryPoints'];
- duration: number;
+export interface TimingsReport {
+ tapables?: TimingsMap;
+ loaders?: TimingsMap;
+ modules?: TimingsMap;
+}
+
+export interface Report {
+ timings: TimingsReport;
}
export type BundlerContext = {
start: number;
- report: Report;
- metrics?: MetricToSend[];
- bundler: {
- esbuild?: EsbuildBundlerResult;
- webpack?: Stats;
- };
+ report?: Report;
};
-export interface EsbuildIndexedObject {
- entryNames: Map;
- inputsDependencies: { [key: string]: Set };
- outputsDependencies: { [key: string]: Set };
-}
-
-export interface WebpackIndexedObject {
- modulesPerName: { [key: string]: Module };
- chunksPerId: { [key: string]: Chunk };
- entriesPerChunkId: { [key: string]: Entry };
-}
-
export interface ModuleGraph {
getModule(dependency: Dependency): Module;
getIssuer(module: Module): Module;
@@ -105,21 +97,6 @@ export interface Compilation {
};
}
-export interface Stats {
- toJson(opts: { children: boolean }): StatsJson;
- endTime: number;
- startTime: number;
- compilation: {
- assets: { [key: string]: { size: number } };
- fileDependencies: Set;
- emittedAssets: Set;
- warnings: string[];
- modules: Set | Module[];
- chunks: Set | Chunk[];
- entries: any[] | Map;
- };
-}
-
export interface Chunk {
id: string;
size: number;
@@ -129,33 +106,6 @@ export interface Chunk {
parents: string[];
}
-export interface Asset {
- name: string;
- size: number;
- chunks: string[];
-}
-
-export interface Entry {
- name: string;
- chunks: string[];
-}
-
-export interface Entries {
- [key: string]: Entry;
-}
-
-export interface StatsJson {
- entrypoints: {
- [key: string]: Entry;
- };
- chunks: Chunk[];
- modules: Module[];
- assets: Asset[];
- warnings: string[];
- errors: string[];
- time: number;
-}
-
export interface Hook {
tap?: Tap;
tapAsync?: TapAsync;
@@ -170,42 +120,8 @@ export interface Tapable {
};
}
-export interface Compiler extends Tapable {
- options: {};
- hooks: {
- thisCompilation: { tap(opts: any, callback: (compilation: Compilation) => void): void };
- done: {
- tap(opts: any, callback: (stats: Stats) => void): void;
- tapPromise(opts: any, callback: (stats: Stats) => void): Promise;
- };
- };
-}
-
export type TAP_TYPES = 'default' | 'async' | 'promise';
-export interface TimingsReport {
- tapables?: TimingsMap;
- loaders?: TimingsMap;
- modules?: TimingsMap;
-}
-
-export interface Report {
- timings: TimingsReport;
- dependencies: LocalModules;
-}
-
-export interface EsbuildStats extends Metafile {
- warnings: Message[];
- errors: Message[];
- entrypoints: BuildOptions['entryPoints'];
- duration: number;
-}
-
-export interface BundlerStats {
- webpack?: Stats;
- esbuild?: EsbuildStats;
-}
-
export interface ValueContext {
type: string;
name: string;
@@ -281,19 +197,3 @@ export interface Event {
timings: Value;
loaders: string[];
}
-
-export interface LocalModule {
- name: string;
- size: number;
- chunkNames: string[];
- dependencies: string[];
- dependents: string[];
-}
-
-export interface LocalModules {
- [key: string]: LocalModule;
-}
-
-export interface ModulesResult {
- modules: LocalModules;
-}
diff --git a/packages/plugins/telemetry/src/webpack-plugin/index.ts b/packages/plugins/telemetry/src/webpack-plugin/index.ts
index adc4cc0b..ab44dbbe 100644
--- a/packages/plugins/telemetry/src/webpack-plugin/index.ts
+++ b/packages/plugins/telemetry/src/webpack-plugin/index.ts
@@ -2,32 +2,26 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import { getLogger } from '@dd/core/log';
import type { GlobalContext } from '@dd/core/types';
import type { UnpluginOptions } from 'unplugin';
-import { validateOptions } from '../common/helpers';
-import { output } from '../common/output';
-import { sendMetrics } from '../common/sender';
import { PLUGIN_NAME } from '../constants';
-import type { Compilation, Report, Stats, OptionsWithTelemetry, BundlerContext } from '../types';
+import type { Compilation, BundlerContext } from '../types';
import { Loaders } from './loaders';
-import { Modules } from './modules';
import { Tapables } from './tapables';
export const getWebpackPlugin = (
- opt: OptionsWithTelemetry,
- ctx: GlobalContext,
+ bundlerContext: BundlerContext,
+ globalContext: GlobalContext,
): UnpluginOptions['webpack'] => {
return async (compiler) => {
+ globalContext.build.start = Date.now();
+
const HOOK_OPTIONS = { name: PLUGIN_NAME };
- const options = validateOptions(opt);
- const logger = getLogger(opt.logLevel, 'telemetry');
- const modules = new Modules(ctx.cwd, options);
- const tapables = new Tapables(ctx.cwd, options);
- const loaders = new Loaders(ctx.cwd, options);
+ const tapables = new Tapables(globalContext.cwd);
+ const loaders = new Loaders(globalContext.cwd);
// @ts-expect-error - webpack 4 and 5 nonsense.
tapables.throughHooks(compiler);
@@ -43,36 +37,21 @@ export const getWebpackPlugin = (
compilation.hooks.succeedModule.tap(HOOK_OPTIONS, (module) => {
loaders.succeedModule(module, compilation);
});
-
- compilation.hooks.afterOptimizeTree.tap(HOOK_OPTIONS, (chunks, mods) => {
- modules.afterOptimizeTree(chunks, mods, compilation);
- });
});
- // @ts-expect-error - webpack 4 and 5 nonsense.
- compiler.hooks.done.tapPromise(HOOK_OPTIONS, async (stats: Stats) => {
- const start = Date.now();
+ // We're losing some tracing from plugins by using `afterEmit` instead of `done` but
+ // it allows us to centralize the common process better.
+ compiler.hooks.afterEmit.tapPromise(HOOK_OPTIONS, async (compilation) => {
const { timings: tapableTimings } = tapables.getResults();
const { loaders: loadersTimings, modules: modulesTimings } = loaders.getResults();
- const { modules: modulesDeps } = modules.getResults();
- const report: Report = {
+ bundlerContext.report = {
timings: {
tapables: tapableTimings,
loaders: loadersTimings,
modules: modulesTimings,
},
- dependencies: modulesDeps,
};
-
- const context: BundlerContext = {
- start,
- report,
- bundler: { webpack: stats },
- };
-
- await output(context, options, logger, ctx);
- await sendMetrics(context.metrics, opt, logger);
});
};
};
diff --git a/packages/plugins/telemetry/src/webpack-plugin/loaders.ts b/packages/plugins/telemetry/src/webpack-plugin/loaders.ts
index 24de401d..570f8d5d 100644
--- a/packages/plugins/telemetry/src/webpack-plugin/loaders.ts
+++ b/packages/plugins/telemetry/src/webpack-plugin/loaders.ts
@@ -5,15 +5,13 @@
import { performance } from 'perf_hooks';
import { getDisplayName, getModuleName, getLoaderNames } from '../common/helpers';
-import type { Module, Event, Timing, Compilation, TimingsMap, TelemetryOptions } from '../types';
+import type { Module, Event, Timing, Compilation, TimingsMap } from '../types';
export class Loaders {
- constructor(cwd: string, options: TelemetryOptions) {
- this.options = options;
+ constructor(cwd: string) {
this.cwd = cwd;
}
cwd: string;
- options: TelemetryOptions;
started: { [key: string]: Event } = {};
finished: Event[] = [];
diff --git a/packages/plugins/telemetry/src/webpack-plugin/modules.ts b/packages/plugins/telemetry/src/webpack-plugin/modules.ts
deleted file mode 100644
index eaeffb80..00000000
--- a/packages/plugins/telemetry/src/webpack-plugin/modules.ts
+++ /dev/null
@@ -1,116 +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 { getDisplayName, getModuleName, getModuleSize } from '../common/helpers';
-import type {
- Module,
- LocalModule,
- ModulesResult,
- Compilation,
- Dependency,
- TelemetryOptions,
-} from '../types';
-
-export class Modules {
- constructor(cwd: string, options: TelemetryOptions) {
- this.options = options;
- this.cwd = cwd;
- }
- cwd: string;
- options: TelemetryOptions;
- storedModules: { [key: string]: LocalModule } = {};
- storedDependents: { [key: string]: Set } = {};
-
- // In Webpack 5, using dep.module throws an error.
- // It's advised to use ModuleGraph API instead (not available in previous versions).
- getModule(dep: Dependency, compilation: Compilation): Module | undefined {
- try {
- return dep.module;
- } catch (e) {
- return compilation.moduleGraph?.getModule(dep);
- }
- }
-
- getChunks(module: Module, compilation: Compilation): Set {
- return module._chunks || compilation.chunkGraph?.getModuleChunks(module);
- }
-
- getLocalModule(
- name: string,
- module: Module,
- compilation: Compilation,
- opts?: Partial,
- ): LocalModule {
- const localModule: LocalModule = {
- name: getDisplayName(name),
- size: getModuleSize(module),
- chunkNames: Array.from(this.getChunks(module, compilation)).map((c) => c.name),
- dependencies: [],
- dependents: [],
- ...opts,
- };
-
- return localModule;
- }
-
- afterOptimizeTree(chunks: any, modules: Module[], compilation: Compilation) {
- const moduleMap: { [key: string]: Module } = {};
-
- for (const module of modules) {
- const moduleName = getModuleName(module, compilation, this.cwd);
- moduleMap[moduleName] = module;
- let dependencies = module.dependencies
- // Ensure it's a module because webpack register as dependency
- // a lot of different stuff that are not modules.
- // RequireHeaderDependency, ConstDepependency, ...
- .filter((dep) => this.getModule(dep, compilation))
- .map((dep) =>
- getModuleName(this.getModule(dep, compilation)!, compilation, this.cwd),
- );
-
- // If we've already encounter this module, merge its dependencies.
- if (this.storedModules[moduleName]) {
- dependencies = [...dependencies, ...this.storedModules[moduleName].dependencies];
- }
-
- // Make dependencies unique and format their names.
- dependencies = [...new Set(dependencies)];
-
- this.storedModules[moduleName] = this.getLocalModule(moduleName, module, compilation, {
- dependencies,
- });
-
- // Update the dependents store once we have all dependencies
- for (const dep of dependencies) {
- this.storedDependents[dep] = this.storedDependents[dep] || new Set();
- if (!this.storedDependents[dep].has(moduleName)) {
- this.storedDependents[dep].add(moduleName);
- }
- }
- }
-
- // Re-assign dependents to modules.
- for (const storedDepName in this.storedDependents) {
- if (Object.hasOwn(this.storedDependents, storedDepName)) {
- if (!this.storedModules[storedDepName]) {
- this.storedModules[storedDepName] = this.getLocalModule(
- storedDepName,
- moduleMap[storedDepName],
- compilation,
- );
- }
- // Assign dependents.
- this.storedModules[storedDepName].dependents = Array.from(
- this.storedDependents[storedDepName],
- );
- }
- }
- }
-
- getResults(): ModulesResult {
- return {
- modules: this.storedModules,
- };
- }
-}
diff --git a/packages/plugins/telemetry/src/webpack-plugin/tapables.ts b/packages/plugins/telemetry/src/webpack-plugin/tapables.ts
index 3b029f88..35dd0d11 100644
--- a/packages/plugins/telemetry/src/webpack-plugin/tapables.ts
+++ b/packages/plugins/telemetry/src/webpack-plugin/tapables.ts
@@ -5,6 +5,7 @@
import { performance } from 'perf_hooks';
import { getPluginName, getValueContext } from '../common/helpers';
+import { PLUGIN_NAME } from '../constants';
import type {
MonitoredTaps,
Tapable,
@@ -18,16 +19,13 @@ import type {
Tap,
Hook,
Timing,
- TelemetryOptions,
} from '../types';
export class Tapables {
- constructor(cwd: Tapables['cwd'], options: Tapables['options']) {
- this.options = options;
+ constructor(cwd: Tapables['cwd']) {
this.cwd = cwd;
}
cwd: string;
- options: TelemetryOptions;
monitoredTaps: MonitoredTaps = {};
tapables: Tapable[] = [];
hooks: Hooks = {};
@@ -198,6 +196,11 @@ export class Tapables {
return;
}
+ // Skip the current plugin.
+ if (tapableName.includes(PLUGIN_NAME)) {
+ return;
+ }
+
if (!this.hooks[tapableName]) {
this.hooks[tapableName] = [];
}
diff --git a/packages/tests/jest.config.js b/packages/tests/jest.config.js
index a938b0a1..72f3f5a8 100644
--- a/packages/tests/jest.config.js
+++ b/packages/tests/jest.config.js
@@ -12,4 +12,5 @@ module.exports = {
testEnvironment: 'node',
testMatch: ['**/*.test.*'],
roots: ['./src/'],
+ setupFilesAfterEnv: ['/src/setupTests.ts'],
};
diff --git a/packages/tests/src/helpers/runBundlers.ts b/packages/tests/src/helpers/runBundlers.ts
index aaa1aaac..bb998e8f 100644
--- a/packages/tests/src/helpers/runBundlers.ts
+++ b/packages/tests/src/helpers/runBundlers.ts
@@ -122,7 +122,7 @@ export const runRollup = async (
return result;
};
-type Bundler = {
+export type Bundler = {
name: string;
run: (opts: Options, config?: any) => Promise;
version: string;
@@ -172,9 +172,13 @@ export const BUNDLERS: Bundler[] = [
export const runBundlers = async (
pluginOverrides: Partial = {},
bundlerOverrides: Record = {},
+ bundlers?: string[],
) => {
const results: any[] = [];
rmSync(defaultDestination, { recursive: true, force: true, maxRetries: 3 });
+ const bundlersToRun = BUNDLERS.filter(
+ (bundler) => !bundlers || bundlers.includes(bundler.name),
+ );
// Needed to avoid SIGHUP errors with exit code 129.
// Specifically for vite, which is the only one that crashes with this error when ran more than once.
// TODO: Investigate why vite crashed when ran more than once.
@@ -183,8 +187,8 @@ export const runBundlers = async (
// Running vite and webpack together will crash the process with exit code 129.
// Not sure why, but we need to isolate them.
// TODO: Investigate why vite and webpack can't run together.
- const webpackBundlers = BUNDLERS.filter((bundler) => bundler.name.startsWith('webpack'));
- const otherBundlers = BUNDLERS.filter((bundler) => !bundler.name.startsWith('webpack'));
+ const webpackBundlers = bundlersToRun.filter((bundler) => bundler.name.startsWith('webpack'));
+ const otherBundlers = bundlersToRun.filter((bundler) => !bundler.name.startsWith('webpack'));
const runBundlerFunction = (bundler: Bundler) => {
let bundlerOverride = {};
diff --git a/packages/tests/src/helpers/toBeWithinRange.ts b/packages/tests/src/helpers/toBeWithinRange.ts
new file mode 100644
index 00000000..54a6c125
--- /dev/null
+++ b/packages/tests/src/helpers/toBeWithinRange.ts
@@ -0,0 +1,38 @@
+// 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 toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> =
+ // `floor` and `ceiling` get types from the line above
+ // it is recommended to type them as `unknown` and to validate the values
+ function toBeWithinRange(actual, floor, ceiling) {
+ if (
+ typeof actual !== 'number' ||
+ typeof floor !== 'number' ||
+ typeof ceiling !== 'number'
+ ) {
+ throw new TypeError('These must be of type number!');
+ }
+
+ const pass = actual >= floor && actual <= ceiling;
+ if (pass) {
+ return {
+ message: () =>
+ // `this` context will have correct typings
+ `expected ${this.utils.printReceived(
+ actual,
+ )} not to be within range ${this.utils.printExpected(`${floor} - ${ceiling}`)}`,
+ pass: true,
+ };
+ } else {
+ return {
+ message: () =>
+ `expected ${this.utils.printReceived(
+ actual,
+ )} to be within range ${this.utils.printExpected(`${floor} - ${ceiling}`)}`,
+ pass: false,
+ };
+ }
+ };
diff --git a/packages/tests/src/plugins/telemetry/common/aggregator.test.ts b/packages/tests/src/plugins/telemetry/common/aggregator.test.ts
index 308d8175..25b0b52c 100644
--- a/packages/tests/src/plugins/telemetry/common/aggregator.test.ts
+++ b/packages/tests/src/plugins/telemetry/common/aggregator.test.ts
@@ -3,12 +3,13 @@
// Copyright 2019-Present Datadog, Inc.
import { getMetrics } from '@dd/telemetry-plugins/common/aggregator';
+import { getContextMock } from '@dd/tests/helpers/mocks';
import { mockOptionsDD, mockReport } from '@dd/tests/plugins/telemetry/testHelpers';
describe('Telemetry Aggregator', () => {
test('It should aggregate metrics without throwing.', () => {
expect(() => {
- getMetrics(mockOptionsDD, mockReport, {}, '');
+ getMetrics(getContextMock(), mockOptionsDD, mockReport);
}).not.toThrow();
});
});
diff --git a/packages/tests/src/plugins/telemetry/common/helpers.test.ts b/packages/tests/src/plugins/telemetry/common/helpers.test.ts
index b86e4ca8..b98cb179 100644
--- a/packages/tests/src/plugins/telemetry/common/helpers.test.ts
+++ b/packages/tests/src/plugins/telemetry/common/helpers.test.ts
@@ -2,11 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.
-import {
- getModuleName,
- getModuleSize,
- getValueContext,
-} from '@dd/telemetry-plugins/common/helpers';
+import { getModuleName, getValueContext } from '@dd/telemetry-plugins/common/helpers';
import { getMockCompilation, getMockModule, mockCompilation } from '../testHelpers';
@@ -33,21 +29,24 @@ describe('Telemetry Helpers', () => {
).toBe('moduleName');
});
- test('It should return the size of a module', () => {
- const module1 = getMockModule({ size: 1 });
- const module2 = getMockModule({ size: () => 2 });
- expect(getModuleSize(module1)).toBe(1);
- expect(getModuleSize(module2)).toBe(2);
- expect(getModuleSize()).toBe(0);
- });
-
test('It should getContext with and without constructor', () => {
const BasicClass: any = function BasicClass() {};
const instance1 = new BasicClass();
const instance2 = new BasicClass();
instance2.constructor = null;
- getValueContext([instance1, instance2]);
- expect(() => {}).not.toThrow();
+ expect(() => {
+ getValueContext([instance1, instance2]);
+ }).not.toThrow();
+
+ const context = getValueContext([instance1, instance2]);
+ expect(context).toEqual([
+ {
+ type: 'BasicClass',
+ },
+ {
+ type: 'object',
+ },
+ ]);
});
});
diff --git a/packages/tests/src/plugins/telemetry/common/metrics/esbuild.test.ts b/packages/tests/src/plugins/telemetry/common/metrics/esbuild.test.ts
deleted file mode 100644
index 89987614..00000000
--- a/packages/tests/src/plugins/telemetry/common/metrics/esbuild.test.ts
+++ /dev/null
@@ -1,144 +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 { getDisplayName } from '@dd/telemetry-plugins/common/helpers';
-import {
- getModules,
- getIndexed,
- getEntries,
- getAssets,
-} from '@dd/telemetry-plugins/common/metrics/esbuild';
-import type { Metric, EsbuildStats } from '@dd/telemetry-plugins/types';
-import { PROJECT_ROOT } from '@dd/tests/helpers/mocks';
-import { runEsbuild } from '@dd/tests/helpers/runBundlers';
-import { prefixPath } from '@dd/tests/plugins/telemetry/testHelpers';
-import fs from 'fs';
-import path from 'path';
-
-describe('Telemetry ESBuild Metrics', () => {
- // This will be removed in a follow-up PR.
- describe.skip(`Esbuild`, () => {
- let statsJson: EsbuildStats;
- const OUTPUT = path.resolve(PROJECT_ROOT, `./esbuild-output/`);
-
- afterAll(async () => {
- // Clean
- fs.rmSync(OUTPUT, { force: true, recursive: true });
- });
-
- beforeAll(async () => {
- await runEsbuild(
- {
- auth: {
- apiKey: '',
- },
- telemetry: {
- output: OUTPUT,
- },
- },
- {
- sourcemap: false,
- entryPoints: {
- yolo: prefixPath('./src/file0001.js'),
- cheesecake: prefixPath('./src/file0000.js'),
- },
- outdir: prefixPath('./esbuild-output/dist'),
- },
- );
-
- statsJson = require(path.resolve(OUTPUT, './bundler.json'));
- }, 20000);
-
- describe('Modules', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getModules(statsJson, indexed, PROJECT_ROOT);
- });
-
- test('It should give module metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test(`It should add tags about the entry and the chunk.`, () => {
- for (const metric of metrics) {
- expect(metric.tags).toContain('entryName:yolo');
- expect(metric.tags).toContain('entryName:cheesecake');
- }
- });
-
- test('It should have 1 metric per module.', () => {
- const modules = [
- prefixPath('./src/file0000.js'),
- prefixPath('./src/file0001.js'),
- prefixPath('./workspaces/app/file0000.js'),
- prefixPath('./workspaces/app/file0001.js'),
- ];
-
- for (const module of modules) {
- const modulesMetrics = metrics.filter((m) =>
- m.tags.includes(`moduleName:${getDisplayName(module)}`),
- );
- expect(modulesMetrics.length).toBe(1);
- }
- });
- });
-
- describe('Entries', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getEntries(statsJson, indexed, PROJECT_ROOT);
- });
-
- test('It should give entries metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test('It should give 3 metrics per entry.', () => {
- const entries = ['yolo', 'cheesecake'];
-
- for (const entry of entries) {
- const entriesMetrics = metrics.filter((m) =>
- m.tags.includes(`entryName:${entry}`),
- );
- expect(entriesMetrics.length).toBe(3);
- }
- });
- });
-
- describe('Assets', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getAssets(statsJson, indexed, PROJECT_ROOT);
- });
-
- test('It should give assets metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test('It should give 1 metric per asset.', () => {
- const assets = ['yolo\\.js', 'cheesecake\\.js'];
- for (const asset of assets) {
- const rx = new RegExp(`^assetName:.*${asset}$`);
- const assetsMetrics = metrics.filter((m) =>
- m.tags.some((tag: string) => rx.test(tag)),
- );
- expect(assetsMetrics.length).toBe(1);
- }
- });
-
- test(`It should add tags about the entry.`, () => {
- for (const metric of metrics) {
- expect(metric.tags).toContain('entryName:yolo');
- expect(metric.tags).toContain('entryName:cheesecake');
- }
- });
- });
- });
-});
diff --git a/packages/tests/src/plugins/telemetry/common/metrics/webpack.test.ts b/packages/tests/src/plugins/telemetry/common/metrics/webpack.test.ts
deleted file mode 100644
index b2245adb..00000000
--- a/packages/tests/src/plugins/telemetry/common/metrics/webpack.test.ts
+++ /dev/null
@@ -1,193 +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 { getDisplayName } from '@dd/telemetry-plugins/common/helpers';
-import {
- getModules,
- getIndexed,
- getEntries,
- getChunks,
- getAssets,
-} from '@dd/telemetry-plugins/common/metrics/webpack';
-import type { StatsJson, Metric } from '@dd/telemetry-plugins/types';
-import { PROJECT_ROOT } from '@dd/tests/helpers/mocks';
-import { runWebpack, runWebpack4 } from '@dd/tests/helpers/runBundlers';
-import fs from 'fs';
-import path from 'path';
-
-describe.skip('Telemetry Webpack Metrics', () => {
- for (const version of [4, 5]) {
- describe(`Webpack ${version}`, () => {
- let statsJson: StatsJson;
- const OUTPUT = path.resolve(PROJECT_ROOT, `./webpack${version}-output/`);
-
- afterAll(async () => {
- // Clean
- fs.rmSync(OUTPUT, { force: true, recursive: true });
- });
-
- beforeAll(async () => {
- const runFn = version === 5 ? runWebpack : runWebpack4;
- await runFn(
- {
- auth: {
- apiKey: '',
- },
- telemetry: {
- output: OUTPUT,
- },
- },
- {
- context: PROJECT_ROOT,
- devtool: false,
- entry: {
- cheesecake: './src/file0000.js',
- yolo: './src/file0001.js',
- },
- output: {
- path: path.resolve(PROJECT_ROOT, `./webpack${version}-output/`),
- filename: '[name].js',
- chunkFilename: '[name].[contenthash].js',
- },
- },
- );
-
- statsJson = require(path.resolve(OUTPUT, './bundler.json'));
- }, 20000);
-
- describe('Modules', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getModules(statsJson, indexed, PROJECT_ROOT);
- });
-
- test('It should give module metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test(`It should filter out webpack's modules.`, () => {
- expect(
- metrics.find((m) => {
- return m.tags.find((t) => /^moduleName:webpack\/runtime/.test(t));
- }),
- ).toBeUndefined();
- });
-
- test(`It should add tags about the entry and the chunk.`, () => {
- for (const metric of metrics) {
- expect(metric.tags).toContain('entryName:yolo');
- expect(metric.tags).toContain('entryName:cheesecake');
- expect(metric.tags).toContain('chunkName:yolo');
- expect(metric.tags).toContain('chunkName:cheesecake');
- }
- });
-
- test('It should have 1 metric per module.', () => {
- const modules = [
- './src/file0000.js',
- './src/file0001.js',
- './workspaces/app/file0000.js',
- './workspaces/app/file0001.js',
- ];
-
- for (const module of modules) {
- const modulesMetrics = metrics.filter((m) =>
- m.tags.includes(`moduleName:${getDisplayName(module)}`),
- );
- expect(modulesMetrics.length).toBe(1);
- }
- });
- });
-
- describe('Entries', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getEntries(statsJson, indexed);
- });
-
- test('It should give entries metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test('It should give 4 metrics per entry.', () => {
- const entries = ['yolo', 'cheesecake'];
-
- for (const entry of entries) {
- const entriesMetrics = metrics.filter((m) =>
- m.tags.includes(`entryName:${entry}`),
- );
- expect(entriesMetrics.length).toBe(4);
- }
- });
- });
-
- describe('Chunks', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getChunks(statsJson, indexed);
- });
-
- test('It should give chunks metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test('It should give 2 metrics per chunk.', () => {
- const chunks = ['yolo', 'cheesecake'];
-
- for (const chunk of chunks) {
- const chunksMetrics = metrics.filter((m) =>
- m.tags.includes(`chunkName:${chunk}`),
- );
- expect(chunksMetrics.length).toBe(2);
- }
- });
-
- test(`It should add tags about the entry.`, () => {
- for (const metric of metrics) {
- expect(metric.tags.join(',')).toMatch(/entryName:(yolo|cheesecake)/);
- }
- });
- });
-
- describe('Assets', () => {
- let metrics: Metric[];
-
- beforeAll(() => {
- const indexed = getIndexed(statsJson, PROJECT_ROOT);
- metrics = getAssets(statsJson, indexed);
- });
-
- test('It should give assets metrics.', () => {
- expect(metrics.length).not.toBe(0);
- });
-
- test('It should give 1 metric per asset.', () => {
- const assets = ['yolo.js', 'cheesecake.js'];
-
- for (const asset of assets) {
- const assetsMetrics = metrics.filter((m) =>
- m.tags.includes(`assetName:${asset}`),
- );
- expect(assetsMetrics.length).toBe(1);
- }
- });
-
- test(`It should add tags about the entry and the chunk.`, () => {
- for (const metric of metrics) {
- expect(metric.tags).toContain('entryName:yolo');
- expect(metric.tags).toContain('entryName:cheesecake');
- expect(metric.tags).toContain('chunkName:yolo');
- expect(metric.tags).toContain('chunkName:cheesecake');
- }
- });
- });
- });
- }
-});
diff --git a/packages/tests/src/plugins/telemetry/common/output/files.test.ts b/packages/tests/src/plugins/telemetry/common/output/files.test.ts
index b8852481..d41e291f 100644
--- a/packages/tests/src/plugins/telemetry/common/output/files.test.ts
+++ b/packages/tests/src/plugins/telemetry/common/output/files.test.ts
@@ -6,17 +6,18 @@ import { outputFiles } from '@dd/telemetry-plugins/common/output/files';
import type { OutputOptions } from '@dd/telemetry-plugins/types';
import { mockLogger, mockReport } from '@dd/tests/plugins/telemetry/testHelpers';
import fs from 'fs-extra';
+import { vol } from 'memfs';
import path from 'path';
+jest.mock('fs', () => require('memfs').fs);
+
describe('Telemetry Output Files', () => {
const directoryName = '/test/';
const init = async (output: OutputOptions, cwd: string) => {
await outputFiles(
{
- start: 0,
report: mockReport,
metrics: [],
- bundler: {},
},
output,
mockLogger,
@@ -24,29 +25,48 @@ describe('Telemetry Output Files', () => {
);
};
- const getExistsProms = (output: string) => {
+ const getExists = (output: string) => {
return [
- fs.pathExists(path.join(output, 'dependencies.json')),
- fs.pathExists(path.join(output, 'timings.json')),
- fs.pathExists(path.join(output, 'stats.json')),
- fs.pathExists(path.join(output, 'metrics.json')),
+ fs.pathExistsSync(path.join(output, 'timings.json')),
+ fs.pathExistsSync(path.join(output, 'metrics.json')),
];
};
- afterEach(async () => {
- await fs.remove(path.join(__dirname, directoryName));
+ afterEach(() => {
+ vol.reset();
});
- describe('With boolean', () => {
- test.each([path.join(__dirname, directoryName), `.${directoryName}`])(
- 'It should allow an absolute and relative path',
- async (output) => {
- await init(output, __dirname);
- const exists = await Promise.all(getExistsProms(output));
- expect(exists.reduce((prev, curr) => prev && curr, true));
- },
- );
+ describe('With strings', () => {
+ test.each([
+ { type: 'an absolue', dirPath: path.join(__dirname, directoryName) },
+ { type: 'a relative', dirPath: `.${directoryName}` },
+ ])('It should allow $type path', async ({ type, dirPath }) => {
+ await init(dirPath, __dirname);
+ const absolutePath = type === 'an absolue' ? dirPath : path.resolve(__dirname, dirPath);
+ const exists = getExists(absolutePath);
+
+ expect(exists[0]).toBeTruthy();
+ expect(exists[1]).toBeTruthy();
+ });
});
+
+ describe('With booleans', () => {
+ test('It should output all the files with true.', async () => {
+ await init(true, __dirname);
+ const exists = getExists(__dirname);
+
+ expect(exists[0]).toBeTruthy();
+ expect(exists[1]).toBeTruthy();
+ });
+ test('It should output no files with false.', async () => {
+ await init(false, __dirname);
+ const exists = getExists(__dirname);
+
+ expect(exists[0]).toBeFalsy();
+ expect(exists[1]).toBeFalsy();
+ });
+ });
+
describe('With object', () => {
test('It should output a single file', async () => {
const output = {
@@ -55,12 +75,10 @@ describe('Telemetry Output Files', () => {
};
await init(output, __dirname);
const destination = output.destination;
- const exists = await Promise.all(getExistsProms(destination));
+ const exists = getExists(destination);
- expect(exists[0]).toBeFalsy();
- expect(exists[1]).toBeTruthy();
- expect(exists[2]).toBeFalsy();
- expect(exists[3]).toBeFalsy();
+ expect(exists[0]).toBeTruthy();
+ expect(exists[1]).toBeFalsy();
});
});
});
diff --git a/packages/tests/src/plugins/telemetry/common/output/text.test.ts b/packages/tests/src/plugins/telemetry/common/output/text.test.ts
deleted file mode 100644
index 15baa7bf..00000000
--- a/packages/tests/src/plugins/telemetry/common/output/text.test.ts
+++ /dev/null
@@ -1,49 +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.
-
-describe('Telemetry Renderer', () => {
- test('It should outputWebpack the same with Webpack 5 and 4', () => {
- const { outputWebpack } = require('@dd/telemetry-plugins/common/output/text');
-
- const ar = [{ name: 'element1' }, { name: 'element2' }];
- const obj = { obj0: ar[0], obj1: ar[1] };
- const set = new Set(ar);
- const map = new Map();
- map.set(0, ar[0]);
- map.set(1, ar[1]);
-
- const statsDefault = {
- endTime: 0,
- startTime: 0,
- compilation: {
- warnings: ar,
- fileDependencies: set,
- },
- };
- const statsWebpack4 = {
- ...statsDefault,
- compilation: {
- ...statsDefault.compilation,
- assets: obj,
- modules: ar,
- entries: ar,
- chunks: ar,
- },
- };
- const statsWebpack5 = {
- ...statsDefault,
- compilation: {
- ...statsDefault.compilation,
- emittedAssets: set,
- modules: set,
- entries: map,
- chunks: set,
- },
- };
- const outputWebpack4 = outputWebpack(statsWebpack4);
- const outputWebpack5 = outputWebpack(statsWebpack5);
-
- expect(outputWebpack4).toBe(outputWebpack5);
- });
-});
diff --git a/packages/tests/src/plugins/telemetry/esbuild-plugin/modules.test.ts b/packages/tests/src/plugins/telemetry/esbuild-plugin/modules.test.ts
deleted file mode 100644
index df96d23b..00000000
--- a/packages/tests/src/plugins/telemetry/esbuild-plugin/modules.test.ts
+++ /dev/null
@@ -1,23 +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 { getModulesResults } from '@dd/telemetry-plugins/common/modules';
-import type { LocalModule } from '@dd/telemetry-plugins/types';
-import { mockMetaFile } from '@dd/tests/plugins/telemetry/testHelpers';
-
-describe('Telemetry ESBuild Modules', () => {
- test('It should add module size to the results', () => {
- const results = getModulesResults('', mockMetaFile);
- for (const module of Object.values(results) as LocalModule[]) {
- expect(module.size).toBeDefined();
- }
- });
-
- test('It should add chunkNames to the results', () => {
- const results = getModulesResults('', mockMetaFile);
- for (const module of Object.values(results) as LocalModule[]) {
- expect(module.chunkNames.length).not.toBe(0);
- }
- });
-});
diff --git a/packages/tests/src/plugins/telemetry/index.test.ts b/packages/tests/src/plugins/telemetry/index.test.ts
new file mode 100644
index 00000000..68e81e44
--- /dev/null
+++ b/packages/tests/src/plugins/telemetry/index.test.ts
@@ -0,0 +1,334 @@
+// 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 { getMetrics } from '@dd/telemetry-plugins/common/aggregator';
+import type { MetricToSend } from '@dd/telemetry-plugins/types';
+import type { Bundler } from '@dd/tests/helpers/runBundlers';
+import { BUNDLERS, runBundlers } from '@dd/tests/helpers/runBundlers';
+import path from 'path';
+
+// Used to intercept metrics.
+jest.mock('@dd/telemetry-plugins/common/aggregator', () => {
+ const originalModule = jest.requireActual('@dd/telemetry-plugins/common/aggregator');
+ return {
+ ...originalModule,
+ getMetrics: jest.fn(),
+ };
+});
+
+const getMetricsMocked = jest.mocked(getMetrics);
+
+const entries = {
+ app1: '@dd/tests/fixtures/project/main1.js',
+ app2: '@dd/tests/fixtures/project/main2.js',
+};
+
+const bundlerOverrides = {
+ rollup: {
+ input: entries,
+ },
+ vite: {
+ input: entries,
+ },
+ esbuild: {
+ entryPoints: entries,
+ },
+ webpack5: { entry: entries },
+ webpack4: {
+ // Webpack 4 doesn't support pnp.
+ entry: Object.fromEntries(
+ Object.entries(entries).map(([name, filepath]) => [
+ name,
+ `./${path.relative(process.cwd(), require.resolve(filepath))}`,
+ ]),
+ ),
+ },
+};
+
+const getGetMetricsImplem: (metrics: Record) => typeof getMetrics =
+ (metrics) => (context, options, report) => {
+ const originalModule = jest.requireActual('@dd/telemetry-plugins/common/aggregator');
+ const originalResult = originalModule.getMetrics(context, options, report);
+ metrics[context.bundler.fullName] = originalResult;
+ return originalResult;
+ };
+
+describe('Telemetry Universal Plugin', () => {
+ const tracingMetrics = [
+ 'loaders.count',
+ 'loaders.duration',
+ 'loaders.increment',
+ 'plugins.count',
+ 'plugins.duration',
+ 'plugins.hooks.duration',
+ 'plugins.hooks.increment',
+ 'plugins.increment',
+ ];
+
+ describe('With enableTracing', () => {
+ const metrics: Record = {};
+ // enableTracing is only supported by esbuild and webpack.
+ const activeBundlers = ['esbuild', 'webpack4', 'webpack5'];
+
+ const bundlers = BUNDLERS.filter((bundler) => activeBundlers.includes(bundler.name));
+ const expectations: (Bundler & { expectedMetrics: string[] })[] = [];
+ const webpack4 = bundlers.find((bundler) => bundler.name === 'webpack4');
+ const webpack5 = bundlers.find((bundler) => bundler.name === 'webpack5');
+ const esbuild = bundlers.find((bundler) => bundler.name === 'esbuild');
+
+ // Doing it this way to prevent failing when running tests with --bundlers.
+ if (esbuild) {
+ expectations.push({
+ ...esbuild,
+ // We only have our own plugin, that is skipped,
+ // so we don't have much data, but having this metrics is enough to assert
+ // that enableTracing is working.
+ expectedMetrics: ['plugins.count'],
+ });
+ }
+
+ if (webpack4) {
+ expectations.push({
+ ...webpack4,
+ expectedMetrics: tracingMetrics,
+ });
+ }
+
+ if (webpack5) {
+ expectations.push({
+ ...webpack5,
+ expectedMetrics: tracingMetrics,
+ });
+ }
+
+ // We don't want to crash if there are no bundlers to test here.
+ // Which can happen when using --bundlers.
+ if (expectations.length > 0) {
+ beforeAll(async () => {
+ const pluginConfig: Options = {
+ telemetry: {
+ enableTracing: true,
+ filters: [],
+ },
+ logLevel: 'warn',
+ };
+ // This one is called at initialization, with the initial context.
+ getMetricsMocked.mockImplementation(getGetMetricsImplem(metrics));
+ await runBundlers(pluginConfig, bundlerOverrides, activeBundlers);
+ });
+
+ 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', () => {
+ const metrics: Record = {};
+
+ beforeAll(async () => {
+ const pluginConfig: Options = {
+ telemetry: {
+ filters: [],
+ },
+ logLevel: 'warn',
+ };
+ // This one is called at initialization, with the initial context.
+ getMetricsMocked.mockImplementation(getGetMetricsImplem(metrics));
+ await runBundlers(pluginConfig, bundlerOverrides);
+ });
+
+ const getMetric = (
+ metricName: string,
+ tags: string[] = expect.any(Array),
+ // Using expect.any(Number) as each bundler will bundled things differently.
+ value: number = expect.any(Number),
+ ) => {
+ return {
+ type: 'gauge',
+ tags,
+ metric: metricName,
+ points: [[expect.any(Number), value]],
+ };
+ };
+
+ describe.each(BUNDLERS)('$name - $version', ({ name }) => {
+ test('Should have no tracing metrics', () => {
+ const metricNames = metrics[name].map((metric) => metric.metric).sort();
+ expect(metricNames).toEqual(expect.not.arrayContaining(tracingMetrics));
+ });
+
+ test('Should have generic metrics', () => {
+ expect(metrics[name]).toEqual(
+ expect.arrayContaining([getMetric('modules.count', [], 15)]),
+ );
+ expect(metrics[name]).toEqual(
+ expect.arrayContaining([getMetric('entries.count', [], 2)]),
+ );
+ expect(metrics[name]).toEqual(expect.arrayContaining([getMetric('assets.count')]));
+ expect(metrics[name]).toEqual(
+ // Rollup and Vite have warnings about circular dependencies, where the others don't.
+ expect.arrayContaining([getMetric('warnings.count')]),
+ );
+ expect(metrics[name]).toEqual(
+ expect.arrayContaining([getMetric('errors.count', [], 0)]),
+ );
+ expect(metrics[name]).toEqual(
+ expect.arrayContaining([getMetric('compilation.duration')]),
+ );
+ });
+
+ test('Should have entry metrics', () => {
+ const entryMetrics = metrics[name].filter((metric) =>
+ metric.metric.startsWith('entries'),
+ );
+ expect(entryMetrics).toEqual(
+ expect.arrayContaining([
+ getMetric('entries.size', ['entryName:app1']),
+ getMetric('entries.modules.count', ['entryName:app1'], 14),
+ getMetric('entries.assets.count', ['entryName:app1']),
+ getMetric('entries.size', ['entryName:app2']),
+ getMetric('entries.modules.count', ['entryName:app2'], 5),
+ getMetric('entries.assets.count', ['entryName:app2']),
+ ]),
+ );
+ });
+
+ const getAssetMetric = (
+ type: string,
+ assetName: string,
+ entryName: string,
+ value: number = expect.any(Number),
+ ) => {
+ return getMetric(
+ `assets.${type}`,
+ expect.arrayContaining([`assetName:${assetName}`, `entryName:${entryName}`]),
+ value,
+ );
+ };
+
+ test('Should have asset metrics', () => {
+ const assetMetrics = metrics[name].filter((metric) =>
+ metric.metric.startsWith('assets'),
+ );
+
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([getAssetMetric('size', 'app1.js', 'app1')]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([getAssetMetric('modules.count', 'app1.js', 'app1')]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([getAssetMetric('size', 'app2.js', 'app2')]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([getAssetMetric('modules.count', 'app2.js', 'app2')]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([getAssetMetric('size', 'app1.js.map', 'app1')]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([
+ getAssetMetric('modules.count', 'app1.js.map', 'app1', 1),
+ ]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([getAssetMetric('size', 'app2.js.map', 'app2')]),
+ );
+ expect(assetMetrics).toEqual(
+ expect.arrayContaining([
+ getAssetMetric('modules.count', 'app2.js.map', 'app2', 1),
+ ]),
+ );
+ });
+
+ const getModuleMetric = (
+ type: string,
+ moduleName: string,
+ entryNames: string[],
+ value: number = expect.any(Number),
+ ) => {
+ return getMetric(
+ `modules.${type}`,
+ expect.arrayContaining([
+ `moduleName:${moduleName}`,
+ `moduleType:js`,
+ ...entryNames.map((entryName) => `entryName:${entryName}`),
+ ]),
+ value,
+ );
+ };
+
+ // [name, entryNames, size, dependencies, dependents];
+ const modulesExpectations: [string, string[], number, number, number][] = [
+ ['src/fixtures/project/workspaces/app/file0000.js', ['app1', 'app2'], 30048, 1, 2],
+ ['src/fixtures/project/workspaces/app/file0001.js', ['app1', 'app2'], 4549, 1, 2],
+ ['src/fixtures/project/src/file0001.js', ['app1', 'app2'], 2203, 2, 2],
+ ['src/fixtures/project/src/file0000.js', ['app1', 'app2'], 13267, 2, 2],
+ ['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],
+ ['color-convert/route.js', ['app1'], 2227, 1, 1],
+ ['color-convert/index.js', ['app1'], 1725, 2, 1],
+ ['ansi-styles/index.js', ['app1'], 3574, 1, 1],
+ ['supports-color/browser.js', ['app1'], 67, 0, 1],
+ ['chalk/templates.js', ['app1'], 3133, 0, 1],
+ // Somehow rollup and vite are not reporting the same size.
+ // @ts-ignore - Not sure why it doesn't load the new type.
+ ['chalk/index.js', ['app1'], expect.toBeWithinRange(6437, 6439), 4, 1],
+ ['src/fixtures/project/main1.js', ['app1'], 379, 2, 0],
+ ['src/fixtures/project/main2.js', ['app2'], 272, 1, 0],
+ ];
+
+ describe.each(modulesExpectations)(
+ 'Should have module metrics for %s',
+ (moduleName, entryNames, size, dependencies, dependents) => {
+ test('Should have module size metrics', () => {
+ const moduleMetrics = metrics[name].filter((metric) =>
+ metric.metric.startsWith('modules'),
+ );
+
+ expect(moduleMetrics).toEqual(
+ expect.arrayContaining([
+ getModuleMetric('size', moduleName, entryNames, size),
+ ]),
+ );
+ });
+ test('Should have module dependencies metrics', () => {
+ const moduleMetrics = metrics[name].filter((metric) =>
+ metric.metric.startsWith('modules'),
+ );
+
+ expect(moduleMetrics).toEqual(
+ expect.arrayContaining([
+ getModuleMetric(
+ 'dependencies',
+ moduleName,
+ entryNames,
+ dependencies,
+ ),
+ ]),
+ );
+ });
+ test('Should have module dependents metrics', () => {
+ const moduleMetrics = metrics[name].filter((metric) =>
+ metric.metric.startsWith('modules'),
+ );
+
+ expect(moduleMetrics).toEqual(
+ expect.arrayContaining([
+ getModuleMetric('dependents', moduleName, entryNames, dependents),
+ ]),
+ );
+ });
+ },
+ );
+ });
+ });
+});
diff --git a/packages/tests/src/plugins/telemetry/testHelpers.ts b/packages/tests/src/plugins/telemetry/testHelpers.ts
index 05d737d1..aef19b6b 100644
--- a/packages/tests/src/plugins/telemetry/testHelpers.ts
+++ b/packages/tests/src/plugins/telemetry/testHelpers.ts
@@ -5,11 +5,8 @@
import type { Logger } from '@dd/core/log';
import type { LogLevel, Options } from '@dd/core/types';
import type {
- BundlerStats,
- Stats,
Report,
Compilation,
- Compiler,
OptionsDD,
OptionsWithTelemetry,
OutputOptions,
@@ -18,12 +15,6 @@ import type {
} from '@dd/telemetry-plugins/types';
import type { PluginBuild, Metafile } from 'esbuild';
import esbuild from 'esbuild';
-import path from 'path';
-
-// Have a path prefixed with the cwd.
-export const prefixPath = (modulePath: string) => {
- return path.join('src/fixtures/project', modulePath);
-};
export const getMockBuild = (overrides: Partial): PluginBuild => {
return {
@@ -39,61 +30,6 @@ export const getMockBuild = (overrides: Partial): PluginBuild => {
};
};
-export const mockStats: Stats = {
- toJson: jest.fn(() => ({
- modules: [],
- chunks: [],
- assets: [],
- entrypoints: {},
- warnings: [],
- errors: [],
- time: 0,
- })),
- endTime: 0,
- startTime: 0,
- compilation: {
- assets: {},
- fileDependencies: new Set(),
- emittedAssets: new Set(),
- warnings: [],
- modules: new Set(),
- chunks: new Set(),
- entries: new Map(),
- },
-};
-
-export const mockBuild: PluginBuild = {
- initialOptions: {},
- esbuild,
- resolve: jest.fn(),
- onStart: jest.fn(),
- onEnd: jest.fn(),
- onResolve: jest.fn(),
- onDispose: jest.fn(),
- onLoad: jest.fn(),
-};
-
-export const mockBundler: BundlerStats = {
- webpack: mockStats,
- esbuild: {
- warnings: [],
- errors: [],
- entrypoints: [],
- duration: 0,
- inputs: {},
- outputs: {},
- },
-};
-
-export const mockReport: Report = {
- timings: {
- tapables: new Map(),
- loaders: new Map(),
- modules: new Map(),
- },
- dependencies: {},
-};
-
const mockTapable = { tap: jest.fn() };
export const mockModule: Module = {
name: 'module',
@@ -141,21 +77,6 @@ export const getMockCompilation = (overrides: Partial): Compilation
},
});
-export const mockCompiler: Compiler = {
- options: {},
- hooks: {
- thisCompilation: {
- tap: (opts: any, cb: (c: Compilation) => void) => {
- cb(mockCompilation);
- },
- },
- done: {
- tap: (opts: any, cb: (s: Stats) => void) => cb(mockStats),
- tapPromise: (opts: any, cb: any) => cb(mockStats),
- },
- },
-};
-
export const mockMetaFile: Metafile = {
inputs: {
module1: {
@@ -183,6 +104,14 @@ export const mockMetaFile: Metafile = {
},
};
+export const mockReport: Report = {
+ timings: {
+ tapables: new Map(),
+ loaders: new Map(),
+ modules: new Map(),
+ },
+};
+
export const mockOptions: Options = {
auth: {
apiKey: '',
diff --git a/packages/tests/src/plugins/telemetry/webpack-plugin/modules.test.ts b/packages/tests/src/plugins/telemetry/webpack-plugin/modules.test.ts
deleted file mode 100644
index 967457ae..00000000
--- a/packages/tests/src/plugins/telemetry/webpack-plugin/modules.test.ts
+++ /dev/null
@@ -1,114 +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 { LocalModule, Module, Compilation, Chunk } from '@dd/telemetry-plugins/types';
-import { Modules } from '@dd/telemetry-plugins/webpack-plugin/modules';
-import { mockTelemetryOptions } from '@dd/tests/plugins/telemetry/testHelpers';
-
-describe('Telemetry Modules', () => {
- // Webpack5 is actually throwing an error when using this property.
- const getThrowingDependency = (dep: any) => {
- Object.defineProperty(dep, 'module', {
- get: () => {
- throw new Error();
- },
- });
- return dep;
- };
-
- const getMockedModule = (opts?: Partial): Module => ({
- name: 'Name',
- size: 1,
- loaders: [],
- chunks: [],
- _chunks: new Set(),
- dependencies: [],
- userRequest: '',
- ...opts,
- });
-
- const getMockedChunk = (opts?: { names?: string[] }): Chunk => ({
- id: 'id',
- size: 0,
- modules: [{}],
- files: ['file'],
- names: ['name'],
- parents: (opts && opts.names) || ['parent'],
- });
-
- const mockedModules: Module[] = [
- getMockedModule({
- name: 'moduleWebpack4',
- size: 50,
- _chunks: new Set([
- getMockedChunk({ names: ['chunk1'] }),
- getMockedChunk({ names: ['chunk2'] }),
- ]),
- dependencies: [
- { module: getMockedModule({ name: 'dep1', size: 1 }) },
- { module: getMockedModule({ name: 'dep2', size: 2 }) },
- { module: getMockedModule({ name: 'dep3', size: 3 }) },
- ],
- }),
- getMockedModule({
- name: 'moduleWebpack5',
- size: () => 50,
- dependencies: [
- getThrowingDependency({ name: 'dep1', size: () => 1 }),
- getThrowingDependency({ name: 'dep2', size: () => 2 }),
- getThrowingDependency({ name: 'dep3', size: () => 3 }),
- ],
- }),
- getMockedModule({ name: 'dep1', size: () => 1 }),
- getMockedModule({ name: 'dep2', size: () => 2 }),
- getMockedModule({ name: 'dep3', size: () => 3 }),
- ];
-
- const mockCompilation: Compilation = {
- options: { context: '' },
- moduleGraph: {
- getIssuer: () => getMockedModule(),
- issuer: getMockedModule(),
- getModule(dep: any) {
- return mockedModules[0].dependencies.find(
- (d) => d.module.name === dep.name && d.module,
- )!.module;
- },
- },
- chunkGraph: {
- getModuleChunks(module: any) {
- return mockedModules[0]._chunks;
- },
- },
- hooks: {
- buildModule: { tap: () => {} },
- succeedModule: { tap: () => {} },
- afterOptimizeTree: { tap: () => {} },
- },
- };
-
- const modules = new Modules('', mockTelemetryOptions);
- modules.afterOptimizeTree({}, mockedModules, mockCompilation);
-
- test('It should filter modules the same with Webpack 5 and 4', () => {
- const modulesWebpack4 = modules.storedModules['moduleWebpack4'].dependencies;
- const modulesWebpack5 = modules.storedModules['moduleWebpack5'].dependencies;
-
- expect(modulesWebpack5.length).toBe(modulesWebpack4.length);
- });
-
- test('It should add module size to the results', () => {
- const results = modules.getResults();
- for (const module of Object.values(results.modules) as LocalModule[]) {
- expect(module.size).toBeDefined();
- }
- });
-
- test('It should add chunk names to the results', () => {
- const results = modules.getResults();
- for (const module of Object.values(results.modules) as LocalModule[]) {
- expect(module.chunkNames).toBeDefined();
- }
- });
-});
diff --git a/packages/tests/src/setupTests.ts b/packages/tests/src/setupTests.ts
new file mode 100644
index 00000000..4753149c
--- /dev/null
+++ b/packages/tests/src/setupTests.ts
@@ -0,0 +1,25 @@
+// 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 nock from 'nock';
+
+import { toBeWithinRange } from './helpers/toBeWithinRange.ts';
+
+expect.extend({
+ toBeWithinRange,
+});
+
+declare module 'expect' {
+ interface AsymmetricMatchers {
+ toBeWithinRange(floor: number, ceiling: number): void;
+ }
+ interface Matchers {
+ toBeWithinRange(floor: number, ceiling: number): R;
+ }
+}
+
+global.beforeAll(() => {
+ // Do not send any HTTP requests.
+ nock.disableNetConnect();
+});