You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CONTRIBUTING.md
+30-12Lines changed: 30 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -65,22 +65,40 @@ The SDK supports both CommonJS and ESM module systems, using different intercept
65
65
66
66
#### CommonJS Module Interception
67
67
68
-
-**Package**: `require-in-the-middle`
69
-
-**How it works**: Hooks into `Module.prototype.require` globally
70
-
-**When it activates**: When `require()` is called
71
-
-**Setup**: Automatic - no special flags needed
72
-
-**Use case**: Works for all CommonJS modules
68
+
-**Package**: `require-in-the-middle` (RITM)
69
+
-**How it works**: CJS modules are loaded through a single JavaScript function (`Module._load`). RITM monkey-patches this function so that every `require()` call passes through the patch, giving the SDK a chance to intercept and wrap module exports.
70
+
-**Setup**: Automatic -- no special flags or loader registration needed. Just calling `TuskDrift.initialize()` before other `require()` calls is sufficient.
73
71
74
72
#### ESM Module Interception
75
73
76
-
-**Package**: `import-in-the-middle`, created by [Datadog](https://opensource.datadoghq.com/projects/node/#the-import-in-the-middle-library)
77
-
-**How it works**: Uses Node.js loader hooks to intercept imports before they're cached
78
-
-**When it activates**: During module resolution/loading phase
79
-
-**Setup**: Requires `--import` flag or `module.register()` call
80
-
-**Use case**: Required for ESM modules
81
-
-**Loader file**: `hook.mjs` - re-exports loader hooks from `import-in-the-middle`
74
+
-**Package**: `import-in-the-middle` (IITM), created by [Datadog](https://opensource.datadoghq.com/projects/node/#the-import-in-the-middle-library)
75
+
-**How it works**: Unlike CJS, ESM module loading is handled by Node.js internals (C++), not a patchable JavaScript function. The only way to intercept ESM imports is through Node's official [customization hooks API](https://nodejs.org/api/module.html#customization-hooks) (`module.register`), which runs hook code in a separate loader thread.
76
+
-**Setup**: The SDK automatically registers ESM loader hooks inside `TuskDrift.initialize()` via `module.register()` (see `src/core/esmLoader.ts`). ESM applications must still use `--import` to ensure the init file runs before the application's import graph is resolved. The `hook.mjs` file at the package root is kept for backward compatibility but is no longer required for manual registration.
82
77
83
-
**Key difference**: CommonJS's `require()` is synchronous and sequential, so you can control order. ESM's `import` is hoisted and parallel, requiring loader hooks to intercept before evaluation.
78
+
#### How ESM instrumentation works end-to-end
79
+
80
+
1.**Loader registration**: `initializeEsmLoader()` (called from `TuskDrift.initialize()`) uses `createAddHookMessageChannel()` from IITM to set up a `MessagePort` between the main thread and the loader thread, then calls `module.register('import-in-the-middle/hook.mjs', ...)` to install the loader hooks.
81
+
2.**Module wrapping**: When any ESM module is imported, IITM's `load` hook transforms its source code on the fly, replacing all named exports with getter/setter proxies. The module works normally, but exports now pass through a proxy layer.
82
+
3.**Hook registration**: `TdInstrumentationBase.enable()` creates `new HookImport(['pg'], {}, hookFn)` for each instrumented module. This registers a callback and sends the module name to the loader thread via the `MessagePort` so the loader knows to watch for it.
83
+
4.**Interception at runtime**: When application code accesses a wrapped module's exports (e.g., `import { Client } from 'pg'`), the getter proxy fires, the `hookFn` callback runs, and the SDK patches the export with its instrumented version.
84
+
85
+
For CJS, steps 1-2 are unnecessary -- RITM patches `Module._load` directly in the main thread, and the rest works the same way.
86
+
87
+
#### Why `--import` is still needed for ESM
88
+
89
+
In CJS, `require()` is synchronous and imperative -- putting `require('./tuskDriftInit')` first guarantees it runs before other modules. In ESM, all `import` declarations are hoisted and the entire module graph is resolved before any module-level code executes. The `--import` flag runs the init file in a pre-evaluation phase, ensuring `TuskDrift.initialize()` (and the loader registration) happens before the application's imports are resolved.
90
+
91
+
#### Node.js built-in modules are always CJS
92
+
93
+
Node.js built-in modules (`http`, `https`, `net`, `fs`, etc.) are loaded through the CJS `require()` path internally, even when imported via ESM `import` syntax. This means RITM can intercept them regardless of the application's module system, and the ESM loader hooks are not required for built-in module instrumentation.
94
+
95
+
#### The `registerEsmLoaderHooks` opt-out
96
+
97
+
Because we pass `include: []` during `module.register()`, IITM starts with an empty allowlist and only wraps modules that are explicitly registered via `new Hook([...])` on the main thread (sent to the loader thread over the `MessagePort`). This means only modules the SDK actually instruments get their exports wrapped with getter/setter proxies -- unrelated modules are left untouched. In rare cases, the wrapping can still conflict with non-standard export patterns, native/WASM bindings, or bundler-generated ESM in the instrumented modules themselves. Users can disable this with `registerEsmLoaderHooks: false` in `TuskDrift.initialize()`, which means only CJS-loaded modules will be instrumentable. See `docs/initialization.md` for the user-facing documentation.
98
+
99
+
#### Compatibility with other IITM consumers (Sentry, OpenTelemetry)
100
+
101
+
Multiple SDKs can each call `module.register()` with their own IITM loader instance and `MessagePort`. IITM detects the duplicate initialization (`global.__import_in_the_middle_initialized__`) and logs a warning, but both SDKs' hooks will fire correctly. Patches layer on top of each other -- if Sentry wraps `pg.Client.query` and Drift also wraps it, the final export passes through both wrappers.
84
102
85
103
### When Does an Instrumentation Need Special ESM Handling?
Copy file name to clipboardExpand all lines: docs/initialization.md
+56-46Lines changed: 56 additions & 46 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,26 +20,7 @@ Create a separate file (e.g. `tuskDriftInit.ts` or `tuskDriftInit.js`) to initia
20
20
21
21
**IMPORTANT**: Ensure that `TuskDrift` is initialized before any other telemetry providers (e.g. OpenTelemetry, Sentry, etc.). If not, your existing telemetry may not work properly.
22
22
23
-
### Determining Your Module System
24
-
25
-
Before proceeding, you need to determine whether your application uses **CommonJS** or **ESM** (ECMAScript Modules).
26
-
27
-
The easiest way to determine this is by looking at your import syntax.
28
-
29
-
**If your application uses `require()`:**
30
-
31
-
- Your application is CommonJS (use the CommonJS setup below)
32
-
33
-
**If your application uses `import` statements:**
34
-
35
-
- This could be either CommonJS or ESM, depending on your build configuration
36
-
- Check your compiled output (if you compile to a directory like `dist/`):
37
-
- If the compiled code contains `require()` statements → CommonJS application
38
-
- If the compiled code contains `import` statements → ESM application
39
-
- If you don't compile your code (running source files directly):
40
-
- It is an ESM application
41
-
42
-
### For CommonJS Applications
23
+
The initialization file is the same for both CommonJS and ESM applications. The SDK automatically registers ESM loader hooks when running in an ESM environment (Node.js >= 18.19.0 or >= 20.6.0).
43
24
44
25
```typescript
45
26
// tuskDriftInit.ts or tuskDriftInit.js
@@ -54,31 +35,7 @@ TuskDrift.initialize({
54
35
export { TuskDrift };
55
36
```
56
37
57
-
### For ESM Applications
58
-
59
-
ESM applications require additional setup to properly intercept module imports:
60
-
61
-
```typescript
62
-
// tuskDriftInit.ts
63
-
import { register } from"node:module";
64
-
import { pathToFileURL } from"node:url";
65
-
66
-
// Register the ESM loader
67
-
// This enables interception of ESM module imports
**Why the ESM loader is needed**: ESM imports are statically analyzed and hoisted, meaning all imports are resolved before any code runs. The `register()` call sets up Node.js loader hooks that intercept module imports, allowing the SDK to instrument packages like `postgres`, `http`, etc. Without this, the SDK cannot patch ESM modules.
38
+
> **Note:** ESM applications still require the `--import` flag when starting Node.js. See [Step 2](#2-import-sdk-at-application-entry-point) for details.
82
39
83
40
### Initialization Parameters
84
41
@@ -116,13 +73,36 @@ export { TuskDrift };
116
73
<td><code>1.0</code></td>
117
74
<td>Override sampling rate (0.0 - 1.0) for recording. Takes precedence over <code>TUSK_SAMPLING_RATE</code> env var and config file.</td>
118
75
</tr>
76
+
<tr>
77
+
<td><code>registerEsmLoaderHooks</code></td>
78
+
<td><code>boolean</code></td>
79
+
<td><code>true</code></td>
80
+
<td>Automatically register ESM loader hooks for module interception. Set to <code>false</code> to disable if <code>import-in-the-middle</code> causes issues with certain packages. See <a href="#troubleshooting-esm">Troubleshooting ESM</a>.</td>
81
+
</tr>
119
82
</tbody>
120
83
</table>
121
84
122
85
> **See also:**[Environment Variables guide](./environment-variables.md) for detailed information about environment variables.
123
86
124
87
## 2. Import SDK at Application Entry Point
125
88
89
+
### Determining Your Module System
90
+
91
+
You need to know whether your application uses **CommonJS** or **ESM** (ECMAScript Modules) because the entry point setup differs.
92
+
93
+
**If your application uses `require()`:**
94
+
95
+
- Your application is CommonJS
96
+
97
+
**If your application uses `import` statements:**
98
+
99
+
- This could be either CommonJS or ESM, depending on your build configuration
100
+
- Check your compiled output (if you compile to a directory like `dist/`):
101
+
- If the compiled code contains `require()` statements → CommonJS application
102
+
- If the compiled code contains `import` statements → ESM application
103
+
- If you don't compile your code (running source files directly):
104
+
- It is an ESM application
105
+
126
106
### For CommonJS Applications
127
107
128
108
In your main server file (e.g., `server.ts`, `index.ts`, `app.ts`), require the initialized SDK **at the very top**, before any other requires:
@@ -153,7 +133,7 @@ For ESM applications, you **cannot** control import order within your applicatio
153
133
}
154
134
```
155
135
156
-
**Why `--import` is required for ESM**: In ESM, all `import` statements are hoisted and evaluated before any code runs, making it impossible to control import order within a file. The `--import` flag ensures the SDK initialization (including loader registration) happens in a separate phase before your application code loads, guaranteeing proper module interception.
136
+
**Why `--import` is required for ESM**: In ESM, all `import` statements are hoisted and evaluated before any code runs, making it impossible to control import order within a file. The `--import` flag ensures the SDK initialization happens in a separate phase before your application code loads, guaranteeing proper module interception.
157
137
158
138
### 3. Configure Sampling Rate
159
139
@@ -255,3 +235,33 @@ app.listen(8000, () => {
255
235
console.log("Server started and ready for Tusk Drift");
256
236
});
257
237
```
238
+
239
+
## Troubleshooting ESM
240
+
241
+
The SDK automatically registers ESM loader hooks via [`import-in-the-middle`](https://www.npmjs.com/package/import-in-the-middle) to intercept ES module imports. Only modules that the SDK instruments have their exports wrapped with getter/setter proxies -- unrelated modules are left untouched.
242
+
243
+
In rare cases, the wrapping can cause issues with instrumented packages:
244
+
245
+
-**Non-standard export patterns**: Some packages use dynamic `export *` re-exports or conditional exports that the wrapper's static analysis cannot parse, resulting in runtime syntax errors.
246
+
-**Native or WASM bindings**: Packages with native addons loaded via ESM can conflict with the proxy wrapping mechanism.
247
+
-**Bundler-generated ESM**: Code that was bundled (e.g., by esbuild or webpack) into ESM sometimes produces patterns the wrapper does not handle correctly.
248
+
-**Circular ESM imports**: The proxy layer can interact badly with circular ESM import graphs in some edge cases.
249
+
250
+
If you encounter errors like:
251
+
252
+
```
253
+
SyntaxError: The requested module '...' does not provide an export named '...'
254
+
(node:1234) Error: 'import-in-the-middle' failed to wrap 'file://../../path/to/file.js'
255
+
```
256
+
257
+
You can disable the automatic ESM hook registration:
258
+
259
+
```typescript
260
+
TuskDrift.initialize({
261
+
apiKey: process.env.TUSK_API_KEY,
262
+
env: process.env.NODE_ENV,
263
+
registerEsmLoaderHooks: false,
264
+
});
265
+
```
266
+
267
+
> **Note:** Disabling ESM loader hooks means the SDK will only be able to instrument packages loaded via CommonJS (`require()`). Packages loaded purely through ESM `import` statements will not be intercepted. Node.js built-in modules (like `http`, `https`, `net`) are always loaded through the CJS path internally, so they will continue to be instrumented regardless of this setting.
0 commit comments