Bundler plugins for webpack, Vite, Rollup and esbuild to instrument modules
at build time.
JavaScript
@apm-js-collab/code-transformer exposes the library.
Usage
import * as codeTransformer from "@apm-js-collab/code-transformer";
// The full instrumentation config
const instrumentation = {
// The name of the diagnostics channel
channelName: "my-channel",
// Define the module you'd like to inject tracing channels into
module: {
name: "my-module",
versionRange: ">=1.0.0",
filePath: "./dist/index.js",
},
// Define the function you'd like to instrument
// (e.g., match a method named 'foo' that returns a Promise)
functionQuery: {
methodName: "fetch",
kind: "Async",
},
};
// Create an InstrumentationMatcher with an array of instrumentation configs
const matcher = codeTransformer.create([instrumentation]);
// Get a transformer for a specific module
const transformer = matcher.getTransformer(
"my-module",
"1.2.3",
"./dist/index.js",
);
if (transformer === undefined) {
throw new Error("No transformer found for module");
}
// Transform code
const inputCode = "async function fetch() { return 42; }";
const result = transformer.transform(inputCode, "unknown");
console.log(result.code);
Export Aliases
When a module re-exports a function or class under a different name using
export { local as exported }, you can target the exported name in your
FunctionQuery by setting isExportAlias: true. The transformer will resolve
the alias to the local declaration before matching.
For example, given:
function f(url) { return fetch(url); }
export { f as fetchAliased };
This also works for class exports (e.g., export { MyClass as PublicClass }).
Mutating the Return Value
A subscriber can both observe or mutate a function’s return value
via message.result. Reassigning message.result is useful when
a function returns another function (or object) that you need to
wrap, like a factory that returns a per-request handler.
For synchronous functions, reassign message.result in the
end handler:
const { tracingChannel } = require("node:diagnostics_channel");
tracingChannel("orchestrion:my-framework:create-handler").subscribe({
end(message) {
const original = message.result;
// Replace the returned handler with a wrapped version.
message.result = function wrapped(...args) {
// ...start a span, etc.
return original.apply(this, args);
};
},
});
For asynchronous functions, reassign message.result in the
asyncEnd handler to substitute the value the returned promise
resolves to — for example, to wrap a function the promise resolves
to:
tracingChannel("orchestrion:my-framework:load-handler").subscribe({
asyncEnd(message) {
const original = message.result;
message.result = function wrapped(...args) {
// ...start a span, etc.
return original.apply(this, args);
};
},
});
[!INFO]
Mutating the resolved value of an async function only works
when it returns a native Promise (value instanceof Promise). Promise subclasses and other userland thenables are
side-chained and returned to the caller unchanged so that their
subclass-specific methods (e.g. APIPromise.withResponse())
remain accessible; their resolved value therefore cannot be
mutated, and reassigning message.result for them has no
effect.
On the throw path the original error still propagates; the
substituted return value only applies when the function returns (or
resolves) normally. If no subscriber reassigns message.result, the
original return value is preserved unchanged.
AST Query
The name-based FunctionQuery variants cover the common cases
(named functions, class/object methods, expressions). When you
need to target a node they can’t express, such as an anonymous
function returned by a factory (a decorator factory, a
per-request handler), you can set astQuery to a raw
esquery selector instead.
When present, astQuery chooses the nodes to instrument and
takes precedence over functionQuery‘s matching fields;
functionQuery then only supplies behaviour (kind, index,
callbackIndex) and may be omitted (it defaults to kind: "Sync").
For example, to instrument the decorator returned by a factory:
function Injectable(options) {
return (target) => { /* applied to the decorated class */ };
}
const instrumentation = {
channelName: "injectable-apply",
module: { name: "@nestjs/common", versionRange: ">=8.0.0", filePath: "decorators/core/injectable.decorator.js" },
// Match the arrow returned from `Injectable`. There is no name to target!
astQuery: 'FunctionDeclaration[id.name="Injectable"] ReturnStatement > ArrowFunctionExpression',
functionQuery: { kind: "Sync" },
};
The channel then fires each time the decorator is applied, with
the decorated target available as message.arguments[0], which a
subscriber can mutate, for example to wrap prototype methods.
An astQuery is used verbatim, so it can match any node,
including ones the name-based variants don’t expose, such as
anonymous or deeply nested functions. (Both name-based and
astQuery matching work on synchronous and async functions
alike.)
If an astQuery matches no nodes, the “failed to find injection
points” error includes the selector so it can be debugged.
Custom Transforms
The built-in operators (traceSync, tracePromise,
traceCallback, traceAuto, selected by functionQuery.kind)
cover diagnostics-channel tracing. When you need to rewrite a
matched node differently, register a custom operator with
matcher.addTransform(name, fn) and reference it from a config’s
transform field. When transform is set it takes precedence
over functionQuery.kind.
A custom transform receives the same arguments an esquery
traversal yields, plus the merged instrumentation state, and
mutates the matched node in place:
type CustomTransform = (state, node, parent, ancestry) => void;
state - the config merged with runtime fields
(moduleVersion, moduleType, operator, the resolved
functionQuery, …).
node - the matched AST node (the function/expression selected
by functionQuery or astQuery).
parent, ancestry - the parent node and full ancestor chain
(root last); ancestry[ancestry.length - 1] is the Program.
A custom transform fully owns how node is rewritten. It is not
required to inject a tracing channel at all.
Registering a name matching a built-in transform (traceSync,
tracePromise, traceCallback, traceAuto,
tracingChannelImport, tracingChannelDeclaration) overrides it
everywhere it is dispatched — including when one built-in invokes
another internally (e.g. the trace operators calling
tracingChannelDeclaration, which in turn calls
tracingChannelImport). state.transforms holds the merged map
(built-ins plus registered overrides) used for this dispatch; to
delegate to the original built-in from an override, call it via
the @apm-js-collab/code-transformer/lib/transforms module
directly.
(The CLI accepts the same transforms via the customTransforms
field of a configuration module. See: CLI Tool.)
API Reference
type ModuleType = "esm" | "cjs" | "unknown";
type FunctionKind = "Sync" | "Async" | "Callback" | "Auto";
FunctionQuery Variants
type FunctionQuery =
| // Match class constructor
{ className: string; index?: number | null; isExportAlias?: boolean }
| // Match class method
{
className: string;
methodName: string;
kind: FunctionKind;
index?: number | null;
callbackIndex?: number;
isExportAlias?: boolean;
}
| // Match method on objects
{ methodName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number }
| // Match standalone function
{ functionName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number; isExportAlias?: boolean }
| // Match arrow function or function expression
{ expressionName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number; isExportAlias?: boolean };
| // Match private class methods
{ className: string; privateMethodName: string; kind: FunctionKind; index?: number | null; callbackIndex?: number };
ModuleMatcher
type ModuleMatcher = {
/** Module name */
name: string;
/** Matching semver range */
versionRange: string;
/**
* Relative Unix-style path to the file from the module root (e.g. "lib/index.js")
* Or a regular expression to test against the Unix-style path.
*/
filePath: string | RegExp;
};
InstrumentationConfig
// Behaviour-only fields, used when `astQuery` does the matching.
type FunctionBehavior = {
kind?: FunctionKind;
index?: number | null;
callbackIndex?: number;
};
type InstrumentationConfig =
| {
channelName: string; // Name of the diagnostics channel
module: ModuleMatcher;
functionQuery: FunctionQuery; // Name-based matching
astQuery?: string; // Raw esquery selector; takes precedence over functionQuery matching
transform?: string; // Name of a custom transform registered via addTransform
}
| {
channelName: string;
module: ModuleMatcher;
astQuery: string; // Raw esquery selector chooses the node(s)
functionQuery?: FunctionBehavior; // Behaviour only; matching fields ignored
transform?: string;
};
Gets a transformer for a specific module and file.
Returns a Transformer for the given module, or undefined if there were no
matching instrumentation configurations.
moduleName - Name of the module.
version - Version of the module.
filePath - Relative Unix-style path to the file from the module root (e.g. "lib/index.js"). Windows-style backslash paths are also accepted and will be normalized automatically.
Orchestrion-JS /
@apm-js-collab/code-transformerThis is a library to aid in instrumenting Node.js libraries at build or load time.
It uses an AST walker to inject code that calls Node.js
TracingChannel.You likely don’t want to use this library directly; instead, consider using:
@apm-js-collab/tracing-hooks/requirehooks to instrument modules as they are loaded.apm-js-collab/code-transformer-bundler-pluginsJavaScript
@apm-js-collab/code-transformerexposes the library.Usage
Export Aliases
When a module re-exports a function or class under a different name using
export { local as exported }, you can target the exported name in yourFunctionQueryby settingisExportAlias: true. The transformer will resolve the alias to the local declaration before matching.For example, given:
You can target
fetchAliasedin your config:This also works for class exports (e.g.,
export { MyClass as PublicClass }).Mutating the Return Value
A subscriber can both observe or mutate a function’s return value via
message.result. Reassigningmessage.resultis useful when a function returns another function (or object) that you need to wrap, like a factory that returns a per-request handler.For synchronous functions, reassign
message.resultin theendhandler:For asynchronous functions, reassign
message.resultin theasyncEndhandler to substitute the value the returned promise resolves to — for example, to wrap a function the promise resolves to:On the throw path the original error still propagates; the substituted return value only applies when the function returns (or resolves) normally. If no subscriber reassigns
message.result, the original return value is preserved unchanged.AST Query
The name-based
FunctionQueryvariants cover the common cases (named functions, class/object methods, expressions). When you need to target a node they can’t express, such as an anonymous function returned by a factory (a decorator factory, a per-request handler), you can setastQueryto a raw esquery selector instead.When present,
astQuerychooses the nodes to instrument and takes precedence overfunctionQuery‘s matching fields;functionQuerythen only supplies behaviour (kind,index,callbackIndex) and may be omitted (it defaults tokind: "Sync").For example, to instrument the decorator returned by a factory:
The channel then fires each time the decorator is applied, with the decorated target available as
message.arguments[0], which a subscriber can mutate, for example to wrap prototype methods.An
astQueryis used verbatim, so it can match any node, including ones the name-based variants don’t expose, such as anonymous or deeply nested functions. (Both name-based andastQuerymatching work on synchronous and async functions alike.)If an
astQuerymatches no nodes, the “failed to find injection points” error includes the selector so it can be debugged.Custom Transforms
The built-in operators (
traceSync,tracePromise,traceCallback,traceAuto, selected byfunctionQuery.kind) cover diagnostics-channel tracing. When you need to rewrite a matched node differently, register a custom operator withmatcher.addTransform(name, fn)and reference it from a config’stransformfield. Whentransformis set it takes precedence overfunctionQuery.kind.A custom transform receives the same arguments an esquery traversal yields, plus the merged instrumentation state, and mutates the matched node in place:
state- the config merged with runtime fields (moduleVersion,moduleType,operator, the resolvedfunctionQuery, …).node- the matched AST node (the function/expression selected byfunctionQueryorastQuery).parent,ancestry- the parent node and full ancestor chain (root last);ancestry[ancestry.length - 1]is theProgram.A custom transform fully owns how
nodeis rewritten. It is not required to inject a tracing channel at all.Registering a name matching a built-in transform (
traceSync,tracePromise,traceCallback,traceAuto,tracingChannelImport,tracingChannelDeclaration) overrides it everywhere it is dispatched — including when one built-in invokes another internally (e.g. the trace operators callingtracingChannelDeclaration, which in turn callstracingChannelImport).state.transformsholds the merged map (built-ins plus registered overrides) used for this dispatch; to delegate to the original built-in from an override, call it via the@apm-js-collab/code-transformer/lib/transformsmodule directly.(The CLI accepts the same transforms via the
customTransformsfield of a configuration module. See: CLI Tool.)API Reference
FunctionQueryVariantsModuleMatcherInstrumentationConfigFunctions
Create a matcher for one or more instrumentation configurations.
configs- Array of instrumentation configurations.dcModule- Optional module to importdiagnostics_channelAPI from.InstrumentationMatcherGets a transformer for a specific module and file.
Returns a
Transformerfor the given module, orundefinedif there were no matching instrumentation configurations.moduleName- Name of the module.version- Version of the module.filePath- Relative Unix-style path to the file from the module root (e.g."lib/index.js"). Windows-style backslash paths are also accepted and will be normalized automatically.Registers a custom transform operator under
name, referenced from a config’stransformfield. See Custom Transforms.name- Operator name (also used as the config’stransformvalue).fn-(state, node, parent, ancestry) => void; mutates the matched node.TransformerTransforms the code, injecting tracing as configured.
Returns
{ code, map }.mapwill be undefined if no sourcemap was supplied.code- The JavaScript code to transform.moduleType- The type of module being transformed.sourcemap- Optional existing source map for the code.CLI Tool
The package includes a CLI tool for applying transformations to source files:
CLI Usage
The CLI tool takes two arguments:
transformer.js- A file that exports instrumentation configuration(s)source-file.js- The source file to transform (can be any path, including node_modules)The transformed code is written to stdout, which you can redirect to a file or pipe to other commands.
CLI Example
Create a transformer configuration file:
Apply the transformation:
The transformer configuration file can also export an object with additional options:
License
See LICENSE