@umutcansu/traceflow-babel-plugin
v0.1.3
Published
Babel plugin that auto-instruments JavaScript / TypeScript functions with TraceFlow ENTER/EXIT/CATCH calls. Companion to @umutcansu/traceflow-runtime.
Maintainers
Readme
@umutcansu/traceflow-babel-plugin
Babel plugin that auto-instruments your JavaScript / TypeScript
functions with TraceFlow ENTER / EXIT / CATCH calls — no manual
log statements needed. Companion to
@umutcansu/traceflow-runtime.
// Your code:
function add(a, b) {
return a + b;
}
// What runs after Babel:
import { _getActiveClient as __tf_getClient,
captureException as __tf_capture
} from "@umutcansu/traceflow-runtime";
function add(a, b) {
const __tf_c = __tf_getClient();
const __tf_t0 = Date.now();
__tf_c?.enter("math", "add", { a, b });
try {
return a + b;
} catch (__tf_e) {
__tf_capture(__tf_e);
throw __tf_e;
} finally {
__tf_c?.exit("math", "add", undefined, Date.now() - __tf_t0);
}
}The original body is preserved verbatim inside the try block;
super, arguments, this, and return semantics are not touched.
Install
yarn add -D @umutcansu/traceflow-babel-plugin
yarn add @umutcansu/traceflow-runtime # peerConfigure
Add to your babel.config.js:
module.exports = {
plugins: [
'@umutcansu/traceflow-babel-plugin',
],
};For React Native / Metro projects the same plugins block applies —
Metro picks up babel.config.js automatically.
Then initialise the runtime once at app startup:
import { initTraceFlow } from '@umutcansu/traceflow-runtime';
initTraceFlow({
endpoint: 'https://traceflow.example.com/traces',
appId: 'com.example.myapp',
platform: 'react-native', // or 'web-js'
appVersion: '1.0.0',
token: process.env.TRACEFLOW_TOKEN,
});That's the entire wiring. Functions you write afterwards stream ENTER/EXIT/CATCH events to the configured TraceFlow server with no further code changes.
What gets wrapped
function name(...) { ... }— top-level function declarationsconst fn = (...) => ...andconst fn = function(...) { ... }— arrow + function expressions assigned to a const, property, or member expressionclass C { method(...) { ... } }— including getters, setters, private (#name) methods, and constructors of non-derived classesconst obj = { method(...) { ... } }— object literal shorthand methodsasyncvariants of all of the above
What does NOT get wrapped
- Anonymous callbacks (
arr.map(x => x*2)). Override withinstrumentAnonymous: trueif you really want them — names emit as<anonymous>:<line>. - Generator functions (
function*, async generators). Wrapping these would break lazyyieldsemantics; deferred. - Derived-class constructors (
class B extends A { constructor() { super(); ... } }).super()must be the first statement, andenter()cannot run before it. Other methods of derived classes wrap normally. - Functions with a
@notraceleading comment:/* @notrace */ function quiet() {}. node_modules— third-party code is never instrumented.- The TraceFlow runtime itself — cycle protection.
Options
plugins: [
['@umutcansu/traceflow-babel-plugin', {
enabled: true,
runtimeImport: '@umutcansu/traceflow-runtime',
excludePatterns: [/\.generated\.[jt]sx?$/],
traceArguments: true,
instrumentAnonymous: false,
}],
],| Option | Default | Effect |
|---|---|---|
| enabled | true unless process.env.NODE_ENV === 'production' | Master kill-switch. When false the plugin is a pure no-op. |
| runtimeImport | '@umutcansu/traceflow-runtime' | Module specifier to import runtime helpers from. Override during local development if you point at a tarball or workspace path. |
| excludePatterns | [] | Array of RegExp matched against the absolute filename. Matched files are skipped. Combined with the built-in node_modules and runtime-self skips. |
| traceArguments | true | When false, the ENTER call carries undefined instead of { a, b, ... }. |
| instrumentAnonymous | false | When true, arrow/function expressions with no inferable name are wrapped using <anonymous>:<line> as the method label. |
Compatibility notes
- TypeScript: works fine in projects that use
@babel/preset-typescript. Register the plugin in the sameplugins:block; Babel applies plugins before presets so the visitor sees the original (still-typed) source. - Source maps: preserved. The plugin only modifies function bodies — surrounding declarations, types, and comments stay where they were.
- Idempotency: running the plugin twice over the same AST does not
double-wrap. A
__tfWrappedflag is attached to wrapped nodes during the pass. async: supported as of0.1.0— wraps with the same try/finally shape as sync functions. Try/finally semantics are identical for async, soenterruns before the firstawait,finallyruns after the returned promise settles, and rejections flow throughcatchthen re-throw exactly as before.- Performance: every wrapped call adds one
_getActiveClient()call, one optional-chainedenter, one optional-chainedexit, and a try/finally. When the runtime hasn't been initialised, all of those short-circuit — the cost is two function calls and one timer read. For tight hot loops, exclude the offending file withexcludePatterns.
EXIT result is intentionally undefined
The plugin does not capture each function's return value into the
EXIT event's result slot. Doing so would require rewriting the
function body (IIFE wrap or per-return rewriting), which breaks
super, arguments, and changes call-stack frames. Keep it simple:
the EXIT event tells you the function exited and how long it took;
if you need the return value for a specific call site, use the manual
trace() helper from @umutcansu/traceflow-runtime for that one
function.
Troubleshooting
My traces don't appear.
Confirm initTraceFlow() is called before the wrapped code runs. The
plugin emits __tf_c?.enter(...) — if the runtime hasn't been
initialised, _getActiveClient() returns null and the chain
short-circuits. Add console.log next to initTraceFlow() to verify
it ran.
Hot reload picks up old cached transforms. Metro caches Babel output. After first installing the plugin run:
yarn start --reset-cacheA specific file is too noisy.
Add a regex to excludePatterns (or sprinkle /* @notrace */
on individual functions).
Production builds are too heavy.
Set NODE_ENV=production and the plugin disables itself. Or pass
enabled: false explicitly.
Metro / Hermes: SyntaxError: import declaration must be at top
level of module.
Fixed in 0.1.1. Earlier 0.1.0 injected the runtime import via raw
AST, which left an orphan import in the bundle when Metro's
modules-commonjs pass had already converted other imports to
require(). Hermes rejects bundles containing such orphans. Upgrade
to 0.1.1+:
yarn add -D @umutcansu/traceflow-babel-plugin@^0.1.1The plugin now uses @babel/helper-module-imports#addNamed, which
adapts the syntax to whatever module system the consuming preset
ends up emitting.
Hermes: ReferenceError: Property 'require' doesn't exist at app
boot.
Fixed in 0.1.2. Metro injects virtual polyfill modules whose
filename contains an embedded NUL byte (e.g.
/abs/project/\0polyfill:external-require). 0.1.1 only skipped
filenames starting with \0, so these polyfills were instrumented
and the wrapper's require() call ran before Metro's require
shim was registered. Upgrade to 0.1.2+:
yarn add -D @umutcansu/traceflow-babel-plugin@^0.1.2The plugin now skips any filename containing \0 anywhere, which
covers all known Metro virtual-module conventions.
Hermes: ReferenceError: Property '_destr_0' doesn't exist on a
function with destructured parameters.
Fixed in 0.1.3. Earlier releases emitted shorthand { _destr_0 }
inside the generated ENTER call whenever a param was an
ObjectPattern / ArrayPattern; those names only exist inside
Babel's destructuring transform and are not in scope at the call
site. Upgrade to 0.1.3+:
yarn add -D @umutcansu/traceflow-babel-plugin@^0.1.3The plugin now expands patterns into their actual bound identifiers
(({ user, settings }) → params: { user, settings }). Workaround
for unupgraded versions: set traceArguments: false.
Versioning
Independent semver from the JVM artefacts. 0.x while the API is
stabilising; first stable release will be 1.0.0.
License
MIT
