From de178ecf6141723c884618406f3d3a3ffa485fb6 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Thu, 27 Aug 2026 14:34:32 -0600 Subject: [PATCH 1/4] docs(configuration): pair threads.preload with preloadRequire for dd-trace `dd-trace/register.js` only installs the ESM loader hooks; it never calls `init()`. Configuring `threads.preload: dd-trace/register.js` alone therefore leaves the tracer uninitialized - spans are created and carry plausible trace ids, but they are no-ops and nothing is exported, so the failure is silent. `dd-trace/init` (`threads.preloadRequire`) is the entry that starts the tracer. Replace the claim that `dd-trace/init` "only covers the main thread", show both keys together in the dd-trace example, and cross-reference the two bullets. Behavior is scoped to dd-trace 6.x, since a future major could change `register.js`. Co-Authored-By: Claude Opus 5 --- reference/configuration/options.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 6163665b..38bbb4a8 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -65,11 +65,12 @@ threads: - `maxHeapMemory` — Heap limit per thread (MB) - `heapSnapshotNearLimit` — Write a `.heapsnapshot` file when a thread nears its heap limit (loadable in Chrome DevTools Memory tab); _Default_: `false`. See [Worker Thread Debugging](./debugging.md#heap-snapshots-near-the-limit) - `debug` — Enable Node.js inspector; sub-options: `port`, `startingPort`, `host`, `waitForDebugger`. See [Worker Thread Debugging](./debugging.md) -- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. Use the agent's ESM/register entry — e.g. `dd-trace/register.js`, which registers the loader hooks that instrument worker threads (where Harper runs its work); the plain `dd-trace/init` (`--require`) entry only covers the main thread. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). +- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. Use the agent's ESM/register entry — e.g. `dd-trace/register.js`, which installs the ESM loader hooks that produce automatic instrumentation for `import`-loaded modules. As measured on dd-trace 6.x, that entry only registers the loader hooks and never calls `init()`, so `preload` on its own leaves the tracer uninitialized: it still hands out spans with plausible trace ids, but they are no-ops and nothing is ever exported. Pair it with `preloadRequire: dd-trace/init`, which is the entry that actually starts the tracer. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). ```yaml threads: - preload: dd-trace/register.js + preloadRequire: dd-trace/init # starts the tracer + preload: dd-trace/register.js # ESM loader hooks for automatic instrumentation ``` Or several modules: @@ -81,11 +82,11 @@ threads: - /opt/instrumentation/agent.mjs ``` -- `preloadRequire` — Same as `preload`, but loads modules via Node's `--require` (CommonJS) instead of `--import`. Use this for agents that document the `--require` path and do not need ESM loader hooks (e.g. `dd-trace/init`, Dynatrace OneAgent). Same resolution rules as `preload`. +- `preloadRequire` — Same as `preload`, but loads modules via Node's `--require` (CommonJS) instead of `--import`. Use this for agents that document the `--require` path (e.g. `dd-trace/init`, Dynatrace OneAgent). Same resolution rules as `preload`. For dd-trace, `dd-trace/init` is the entry that starts the tracer, and it does not register the ESM loader hooks — keep `preload: dd-trace/register.js` alongside it, as shown under `preload` above. ```yaml threads: - preloadRequire: dd-trace/init + preloadRequire: dd-trace/init # starts the tracer; pair with preload (see above) ``` --- From 012d38e303d4818cec1be86f2443736ad9d01e29 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Fri, 28 Aug 2026 11:27:27 -0600 Subject: [PATCH 2/4] docs(configuration): make every dd-trace preload example viable The multi-module `threads.preload` example still listed `dd-trace/register.js` under `preload` alone, which reproduces the exact inert-tracer footgun the single-module example was corrected for: anyone copying it gets a worker whose tracer is never initialized and which exports no traces. Add `preloadRequire: dd-trace/init` to that example too. Also record why `dd-trace/initialize.mjs` is not the simpler single-entry alternative it looks like. On dd-trace 6.x it gates both its `init()` call and its `Module.register()` of the loader hook behind `isMainThread`, and its exported `load`/`resolve` hooks only take effect under `--loader`. Harper preloads via `--import` in a worker's `execArgv`, so on a worker thread that entry starts nothing and registers nothing. Co-Authored-By: Claude Opus 5 --- reference/configuration/options.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 38bbb4a8..1dec1db4 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -65,7 +65,7 @@ threads: - `maxHeapMemory` — Heap limit per thread (MB) - `heapSnapshotNearLimit` — Write a `.heapsnapshot` file when a thread nears its heap limit (loadable in Chrome DevTools Memory tab); _Default_: `false`. See [Worker Thread Debugging](./debugging.md#heap-snapshots-near-the-limit) - `debug` — Enable Node.js inspector; sub-options: `port`, `startingPort`, `host`, `waitForDebugger`. See [Worker Thread Debugging](./debugging.md) -- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. Use the agent's ESM/register entry — e.g. `dd-trace/register.js`, which installs the ESM loader hooks that produce automatic instrumentation for `import`-loaded modules. As measured on dd-trace 6.x, that entry only registers the loader hooks and never calls `init()`, so `preload` on its own leaves the tracer uninitialized: it still hands out spans with plausible trace ids, but they are no-ops and nothing is ever exported. Pair it with `preloadRequire: dd-trace/init`, which is the entry that actually starts the tracer. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). +- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. Use the agent's ESM/register entry — e.g. `dd-trace/register.js`, which installs the ESM loader hooks that produce automatic instrumentation for `import`-loaded modules. As measured on dd-trace 6.x, that entry only registers the loader hooks and never calls `init()`, so `preload` on its own leaves the tracer uninitialized: it still hands out spans with plausible trace ids, but they are no-ops and nothing is ever exported. Pair it with `preloadRequire: dd-trace/init`, which is the entry that actually starts the tracer. `dd-trace/initialize.mjs` is not a single-entry shortcut around this pairing: it gates both its `init()` call and its loader-hook registration behind `isMainThread`, so under `--import` on a worker thread it starts nothing and registers nothing. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). ```yaml threads: @@ -73,10 +73,11 @@ threads: preload: dd-trace/register.js # ESM loader hooks for automatic instrumentation ``` -Or several modules: +Or several modules. The `preloadRequire` pairing still applies — an agent listed here is subject to the same rule as when it is the only entry: ```yaml threads: + preloadRequire: dd-trace/init # still what starts the tracer preload: - dd-trace/register.js - /opt/instrumentation/agent.mjs From 222a920bb8a691a09e14fd930fe45a19f4ee70cf Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Mon, 31 Aug 2026 12:38:11 -0600 Subject: [PATCH 3/4] docs(configuration): make threads.preload guidance agent-neutral The preload/preloadRequire bullets described a specific dd-trace pairing as if it were a Harper-validated recipe. Reading dd-trace's entry points establishes what those entries do; it does not establish that Harper's worker execArgv composition exports spans with usable trace context and clean shutdown. Rather than build a Harper end-to-end validation for this, the guidance is now agent-neutral and dd-trace is described as unverified: - Both bullets explain the general mechanism - preload uses --import (ESM, loader hooks), preloadRequire uses --require (CommonJS, runs the module body) - and note that which entry point does which is agent-specific. - dd-trace stays as an illustration of the split-entry case, behind an explicit warning that the values are not a Harper-validated APM configuration and that the specifics are only what dd-trace's own entries do as observed in 6.x. Readers are told to confirm spans arrive at their own collector. - The register.js-does-not-call-init() pairing detail is kept, scoped as a dd-trace observation. - Harper facts are unchanged: bare-specifier resolution against installed components' node_modules, absolute paths accepted, worker threads only, not under Bun. Both dd-trace examples on the page carry the same framing and comments. Closes #625 Co-Authored-By: Claude Opus 5 --- reference/configuration/options.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 1dec1db4..fcb8c29b 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -65,29 +65,35 @@ threads: - `maxHeapMemory` — Heap limit per thread (MB) - `heapSnapshotNearLimit` — Write a `.heapsnapshot` file when a thread nears its heap limit (loadable in Chrome DevTools Memory tab); _Default_: `false`. See [Worker Thread Debugging](./debugging.md#heap-snapshots-near-the-limit) - `debug` — Enable Node.js inspector; sub-options: `port`, `startingPort`, `host`, `waitForDebugger`. See [Worker Thread Debugging](./debugging.md) -- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. Use the agent's ESM/register entry — e.g. `dd-trace/register.js`, which installs the ESM loader hooks that produce automatic instrumentation for `import`-loaded modules. As measured on dd-trace 6.x, that entry only registers the loader hooks and never calls `init()`, so `preload` on its own leaves the tracer uninitialized: it still hands out spans with plausible trace ids, but they are no-ops and nothing is ever exported. Pair it with `preloadRequire: dd-trace/init`, which is the entry that actually starts the tracer. `dd-trace/initialize.mjs` is not a single-entry shortcut around this pairing: it gates both its `init()` call and its loader-hook registration behind `isMainThread`, so under `--import` on a worker thread it starts nothing and registers nothing. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). +- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. `--import` evaluates the module as ESM, so this is the key for an agent's ESM/register entry — the one that installs Node's module loader hooks so modules loaded later by `import` can be instrumented. Installing loader hooks and starting an agent are separate steps, and which of an agent's entry points does which is agent-specific: some ship a single entry that does both, others split them across an `--import` entry and a `--require` entry, in which case set `preload` and `preloadRequire` together. Follow your agent's own documentation for worker-thread setup, and verify the result end to end against your collector. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). + +:::warning The dd-trace values below are unverified +They illustrate the split-entry case; they are not a Harper-validated APM configuration. The specifics are what dd-trace's own entry points do, observed by reading dd-trace 6.x: `dd-trace/register.js` registers the ESM loader hooks and never calls `init()`, so `preload` alone leaves the tracer uninitialized — it still hands out spans with plausible trace ids, but they are no-ops. `dd-trace/init` is the entry that calls `init()`. `dd-trace/initialize.mjs` is not a single-entry shortcut around that pairing: it gates both its `init()` call and its loader-hook registration behind `isMainThread`, so under `--import` on a worker thread it starts nothing and registers nothing. + +What those entry points do on their own is not an end-to-end result: Harper has not been validated to produce exported spans with usable trace context and clean shutdown under this configuration. Confirm the spans you expect actually arrive at your collector before relying on it. +::: ```yaml threads: - preloadRequire: dd-trace/init # starts the tracer + preloadRequire: dd-trace/init # the entry that calls init() preload: dd-trace/register.js # ESM loader hooks for automatic instrumentation ``` -Or several modules. The `preloadRequire` pairing still applies — an agent listed here is subject to the same rule as when it is the only entry: +Or several modules — a split-entry agent still needs both keys when it is one of several entries: ```yaml threads: - preloadRequire: dd-trace/init # still what starts the tracer + preloadRequire: dd-trace/init # the entry that calls init() preload: - dd-trace/register.js - /opt/instrumentation/agent.mjs ``` -- `preloadRequire` — Same as `preload`, but loads modules via Node's `--require` (CommonJS) instead of `--import`. Use this for agents that document the `--require` path (e.g. `dd-trace/init`, Dynatrace OneAgent). Same resolution rules as `preload`. For dd-trace, `dd-trace/init` is the entry that starts the tracer, and it does not register the ESM loader hooks — keep `preload: dd-trace/register.js` alongside it, as shown under `preload` above. +- `preloadRequire` — Same as `preload`, but loads modules via Node's `--require` (CommonJS) instead of `--import`. `--require` runs the module's body, so this is the key for an agent's initialization entry — the entry documented for the `--require` path (e.g. `dd-trace/init`, Dynatrace OneAgent). Same resolution rules as `preload`. When an agent splits initialization from its ESM loader hooks, set both keys; see the note under `preload` above, including its caveat about the dd-trace specifics. ```yaml threads: - preloadRequire: dd-trace/init # starts the tracer; pair with preload (see above) + preloadRequire: dd-trace/init # pair with preload when ESM loader hooks are also needed ``` --- From 7c12d964153db42dbdc85344e711097fc57af90a Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Wed, 2 Sep 2026 13:11:51 -0600 Subject: [PATCH 4/4] docs(configuration): split the preload guidance into short bullets plus prose The preload and preloadRequire bullets had grown to a paragraph each, sitting in a list whose other entries are one-liners. Trim both to a sentence and move the mechanism -- --import for loader hooks, --require for initialization, and why an agent may need both -- into prose under a new heading. Drop the warning framing around the dd-trace example. The observed 6.x behavior is stated as what dd-trace does, with a closing reminder to check the agent's own docs and confirm spans reach the collector, rather than a block declaring the values unvalidated. Co-Authored-By: Claude Opus 5 --- reference/configuration/options.md | 32 +++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index fcb8c29b..d320cab1 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -65,13 +65,24 @@ threads: - `maxHeapMemory` — Heap limit per thread (MB) - `heapSnapshotNearLimit` — Write a `.heapsnapshot` file when a thread nears its heap limit (loadable in Chrome DevTools Memory tab); _Default_: `false`. See [Worker Thread Debugging](./debugging.md#heap-snapshots-near-the-limit) - `debug` — Enable Node.js inspector; sub-options: `port`, `startingPort`, `host`, `waitForDebugger`. See [Worker Thread Debugging](./debugging.md) -- `preload` — Module, or list of modules, to load (via Node's `--import`) before any Harper or application module on each worker thread. Intended for instrumentation/APM agents that must load first to instrument subsequent module loads. `--import` evaluates the module as ESM, so this is the key for an agent's ESM/register entry — the one that installs Node's module loader hooks so modules loaded later by `import` can be instrumented. Installing loader hooks and starting an agent are separate steps, and which of an agent's entry points does which is agent-specific: some ship a single entry that does both, others split them across an `--import` entry and a `--require` entry, in which case set `preload` and `preloadRequire` together. Follow your agent's own documentation for worker-thread setup, and verify the result end to end against your collector. Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md) — so the agent can be shipped as a dependency of a deployed component — and absolute paths are also accepted. Applies to worker threads only (not under Bun). +- `preload` — Module, or list of modules, to load via Node's `--import` before any Harper or application module on each worker thread. Intended for instrumentation and APM agents. Worker threads only (not under Bun). +- `preloadRequire` — The same, but via Node's `--require` (CommonJS). Worker threads only (not under Bun). -:::warning The dd-trace values below are unverified -They illustrate the split-entry case; they are not a Harper-validated APM configuration. The specifics are what dd-trace's own entry points do, observed by reading dd-trace 6.x: `dd-trace/register.js` registers the ESM loader hooks and never calls `init()`, so `preload` alone leaves the tracer uninitialized — it still hands out spans with plausible trace ids, but they are no-ops. `dd-trace/init` is the entry that calls `init()`. `dd-trace/initialize.mjs` is not a single-entry shortcut around that pairing: it gates both its `init()` call and its loader-hook registration behind `isMainThread`, so under `--import` on a worker thread it starts nothing and registers nothing. +### Preloading an instrumentation agent -What those entry points do on their own is not an end-to-end result: Harper has not been validated to produce exported spans with usable trace context and clean shutdown under this configuration. Confirm the spans you expect actually arrive at your collector before relying on it. -::: +An APM or instrumentation agent has to load before the code it instruments. These two keys put a module on each worker thread's startup, ahead of Harper's own modules and your application's. + +The two keys differ in how the module is loaded, and that determines what it can do. `--import` evaluates the module as ESM, which is how an agent installs Node's module loader hooks so modules loaded later by `import` can be instrumented. `--require` runs the module's body, which is how an agent's initialization entry starts it. + +Installing loader hooks and starting an agent are separate jobs, and which of an agent's entry points does which is specific to that agent. Some ship a single entry that does both; others split them across an `--import` entry and a `--require` entry. When they are split, set both keys. + +Bare specifiers resolve against the `node_modules` of your installed [components](../components/overview.md), so an agent can ship as a dependency of a deployed component. Absolute paths also work. + +Follow your agent's own documentation for worker-thread setup, and confirm the telemetry you expect actually reaches your collector. + +#### Example: a split-entry agent + +dd-trace is a split-entry agent, which makes it a useful illustration. On dd-trace 6.x, `dd-trace/register.js` installs the ESM loader hooks but never calls `init()`, so `preload` on its own leaves the tracer uninitialized — it still hands out spans with plausible trace ids, but they are no-ops and nothing is exported. `dd-trace/init` is the entry that calls `init()`. Setting both keys covers both jobs: ```yaml threads: @@ -79,7 +90,9 @@ threads: preload: dd-trace/register.js # ESM loader hooks for automatic instrumentation ``` -Or several modules — a split-entry agent still needs both keys when it is one of several entries: +`dd-trace/initialize.mjs` looks like a single-entry shortcut around that pairing, but it is not one on a worker thread: it gates both its `init()` call and its loader-hook registration behind `isMainThread`, so under `--import` on a worker it starts nothing and registers nothing. + +An agent that is one of several preloaded modules still needs its own pairing: ```yaml threads: @@ -89,12 +102,7 @@ threads: - /opt/instrumentation/agent.mjs ``` -- `preloadRequire` — Same as `preload`, but loads modules via Node's `--require` (CommonJS) instead of `--import`. `--require` runs the module's body, so this is the key for an agent's initialization entry — the entry documented for the `--require` path (e.g. `dd-trace/init`, Dynatrace OneAgent). Same resolution rules as `preload`. When an agent splits initialization from its ESM loader hooks, set both keys; see the note under `preload` above, including its caveat about the dd-trace specifics. - -```yaml -threads: - preloadRequire: dd-trace/init # pair with preload when ESM loader hooks are also needed -``` +The dd-trace behavior described here was observed on 6.x. Check your agent's current documentation, and confirm your spans arrive at your collector. ---