unplugin-datadog-apm
v0.3.0
Published
Unplugin for Datadog APM instrumentation in bundled Node.js applications
Maintainers
Readme
unplugin-datadog-apm
Note: This is an unofficial, community-maintained project. It is not affiliated with, endorsed by, or supported by Datadog, Inc.
Build-time plugin for Datadog APM instrumentation in bundled Node.js applications. Enables dd-trace to instrument bundled modules by wrapping them at build time. This plugin does not instrument anything itself; dd-trace still performs the runtime instrumentation, and this plugin only injects the hooks needed for auto-instrumentation to work in a bundle.
Why?
When you bundle a Node.js application, dd-trace's runtime instrumentation can't hook into the bundled modules because they're no longer loaded through Node's module system. This plugin solves that by:
- CommonJS modules: Wrapping them to publish to the
dd-trace:bundler:loaddiagnostics channel - ESM modules: Creating proxy modules using
import-in-the-middlefor dd-trace interception
Installation
npm i unplugin-datadog-apmRequires dd-trace as a peer dependency:
npm i dd-traceRequirements
- Node.js >=22.0.0
- dd-trace >=5.0.0 (peer dependency)
- ESM-only package: use ESM import syntax in bundler config files
- If you use the Vite adapter, Vite >=7.0.0
Quick Start
1. Add the plugin to your bundler config:
// vite.config.ts
import DatadogAPM from "unplugin-datadog-apm/vite";
export default defineConfig({
plugins: [DatadogAPM()],
ssr: { external: DatadogAPM.externals },
build: { rollupOptions: { external: DatadogAPM.externals } },
});2. Run your app with the --import flag:
node --import unplugin-datadog-apm/register dist/server.jsThat's it! dd-trace will automatically instrument your bundled application.
Configuration
Plugin Options
DatadogAPM({
// Enable debug logging (default: !!process.env.DD_TRACE_DEBUG)
debug: false,
// Additional modules to instrument beyond dd-trace defaults
additionalModules: ["my-custom-module"],
// Modules to exclude from instrumentation
excludeModules: ["some-module"],
});Runtime Configuration
dd-trace is configured at runtime via environment variables:
DD_SERVICE=my-app \
DD_ENV=production \
DD_VERSION=1.2.3 \
node --import unplugin-datadog-apm/register dist/server.jsSee Datadog's Node.js configuration docs for all available options.
Custom Register File
For advanced configuration (custom tracer.use() calls, sampling rules, hooks), create your own register file:
// scripts/datadog-register.mjs
import tracer from "dd-trace";
import {
setupESMImports,
setupOpenTelemetry,
} from "unplugin-datadog-apm/register-helpers";
tracer.init({
service: "my-app",
env: "production",
});
// Silence health check endpoints
tracer.use("http", {
hooks: {
request: (span, req) => {
if (req.url === "/health") {
span.setTag("manual.drop", true);
}
},
},
});
// Add user context to express spans
tracer.use("express", {
hooks: {
request: (span, req) => {
span.setTag("user.id", req.user?.id);
},
},
});
setupOpenTelemetry(tracer);
setupESMImports();Then run with your custom register:
node --import ./scripts/datadog-register.mjs dist/server.jsBundler Setup
Each bundler import provides a .externals property with the list of modules that must be externalized for dd-trace to work correctly.
// vite.config.ts
import DatadogAPM from "unplugin-datadog-apm/vite";
export default defineConfig({
plugins: [DatadogAPM()],
ssr: { external: DatadogAPM.externals },
build: { rollupOptions: { external: DatadogAPM.externals } },
});// rollup.config.js
import DatadogAPM from "unplugin-datadog-apm/rollup";
export default {
plugins: [DatadogAPM()],
external: DatadogAPM.externals,
};// rolldown.config.ts / tsdown.config.ts
import DatadogAPM from "unplugin-datadog-apm/rolldown";
export default {
plugins: [DatadogAPM()],
external: DatadogAPM.externals,
};import { build } from "esbuild";
import DatadogAPM from "unplugin-datadog-apm/esbuild";
build({
plugins: [DatadogAPM()],
external: DatadogAPM.externals,
});// webpack.config.js
import DatadogAPM from "unplugin-datadog-apm/webpack";
export default {
plugins: [DatadogAPM()],
externals: DatadogAPM.externals,
};// rspack.config.js
import DatadogAPM from "unplugin-datadog-apm/rspack";
export default {
plugins: [DatadogAPM()],
externals: DatadogAPM.externals,
};Running Your Application
Always use the --import flag when running your bundled application:
node --import unplugin-datadog-apm/register dist/server.jsFor Docker deployments:
CMD ["node", "--import", "unplugin-datadog-apm/register", "dist/server.js"]For package.json scripts:
{
"scripts": {
"start": "node --import unplugin-datadog-apm/register dist/server.js"
}
}Important Notes
- Externalize runtime deps: Use
DatadogAPM.externalsin your bundler's external config. This exports the list of modules (dd-trace,dc-polyfill,import-in-the-middle, etc.) that must not be bundled for dd-trace to work correctly. - esbuild constraints: See "Limitations and Caveats" for
minify/keepNamesrequirements. - Plugin order: The plugin uses
enforce: 'pre'to run before other transforms. - Module detection: The plugin uses dd-trace's internal utilities to detect which modules are instrumentable and whether they're ESM or CommonJS.
- Git metadata: When git metadata is available at build time, the plugin injects
DD_GIT_REPOSITORY_URLandDD_GIT_COMMIT_SHAinto the output banner. - IAST rewrite: When
DD_IAST_ENABLED=true, the plugin rewrites application JS files using dd-trace's IAST rewriter.
Limitations and Caveats
Bundler and output constraints
- esbuild minify: If you use
minify: true, you must also setkeepNames: trueor the plugin will refuse to bundle (matches dd-trace expectations). - esbuild ESM + CJS deps: esbuild ESM output can emit dynamic-require shims for CommonJS dependencies. Node ESM refuses to execute those shims (for example,
express), so prefer CJS output or ensure dependencies are ESM-only.
Instrumentation scope
- Only modules supported by dd-trace (plus
additionalModules) are wrapped. - Local application modules, unresolved modules, and Node built-ins are not instrumented.
ESM proxy export detection
- Export detection is static. When parsing fails, the proxy falls back to a default export, so named exports can be incomplete.
- Dynamic imports and some
export *chains are not intercepted.
Worker threads
Initialization is skipped in worker threads. The main thread's tracer is inherited, but if you need custom initialization in workers, create a separate register file for them.
Examples
See the examples directory for complete working examples:
How It Works
CommonJS Modules
Wraps CJS modules to publish to the dd-trace diagnostics channel:
(function () {
/* original code */
})(...arguments);
{
const dc = require("dc-polyfill");
const ch = dc.channel("dd-trace:bundler:load");
ch.publish({ module, version, package, path });
module.exports = payload.module;
}ESM Modules
Creates proxy modules using import-in-the-middle:
import { register } from "import-in-the-middle/lib/register.js";
import * as namespace from "original-module";
// Re-exports with getters/setters for interception
register(moduleUrl, _, set, get, rawImportPath);The Register Helper
The --import unplugin-datadog-apm/register flag runs before your application loads, ensuring:
- dd-trace initializes before any modules are imported
- The ESM loader hook is registered via
module.register() - TracerProvider is registered with the OpenTelemetry API
This approach guarantees correct initialization order regardless of bundler or output format.
Legal
This project is MIT licensed.
Datadog is a trademark of Datadog, Inc. This project is not affiliated with Datadog, Inc.
