Before setting up the SDK, ensure you have:
- Completed the CLI wizard
- Obtained an API key from the Tusk Drift dashboard (only required if using Tusk Cloud)
📦 Using Next.js? Next.js applications require a different initialization process. Go to the Next.js Initialization Guide →
For standard Node.js applications (Express, Fastify, plain Node.js, etc.), follow these steps in order to properly initialize the Tusk Drift SDK:
Create a separate file (e.g. tuskDriftInit.ts or tuskDriftInit.js) to initialize the Tusk Drift SDK. This ensures the SDK is initialized as early as possible before any other modules are loaded.
Note: The code examples in this guide use ES module import/export syntax. If your JavaScript project uses CommonJS, adapt the examples to use require()/module.exports instead.
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.
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).
// tuskDriftInit.ts or tuskDriftInit.js
import { TuskDrift } from "@use-tusk/drift-node-sdk";
// Initialize SDK immediately
TuskDrift.initialize({
apiKey: process.env.TUSK_API_KEY,
env: process.env.NODE_ENV,
});
export { TuskDrift };Note: ESM applications still require the
--importflag when starting Node.js. See Step 2 for details.
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
Required if using Tusk Cloud | Your Tusk Drift API key. |
env |
string |
process.env.NODE_ENV |
The environment name. |
logLevel |
'silent' | 'error' | 'warn' | 'info' | 'debug' |
'info' |
The logging level. |
samplingRate |
number |
1.0 |
Override the base sampling rate (0.0 - 1.0) for recording. Takes precedence over TUSK_RECORDING_SAMPLING_RATE and config file base-rate settings. Does not change recording.sampling.mode. |
registerEsmLoaderHooks |
boolean |
true |
Automatically register ESM loader hooks for module interception. Set to false to disable if import-in-the-middle causes issues with certain packages. See Troubleshooting ESM. |
See also: Environment Variables guide for detailed information about environment variables.
You need to know whether your application uses CommonJS or ESM (ECMAScript Modules) because the entry point setup differs.
If your application uses require():
- Your application is CommonJS
If your application uses import statements:
- This could be either CommonJS or ESM, depending on your build configuration
- Check your compiled output (if you compile to a directory like
dist/):- If the compiled code contains
require()statements → CommonJS application - If the compiled code contains
importstatements → ESM application
- If the compiled code contains
- If you don't compile your code (running source files directly):
- It is an ESM application
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:
// e.g. server.ts
import { TuskDrift } from "./tuskDriftInit"; // MUST be the first import
// ... other imports ...
// Your application setup...IMPORTANT: Ensure NO require calls are made before requiring the SDK initialization file. This guarantees proper instrumentation of all dependencies.
For ESM applications, you cannot control import order within your application code because all imports are hoisted. Instead, use the --import flag:
Update your package.json scripts:
{
"scripts": {
"dev": "node --import ./dist/tuskDriftInit.js dist/server.js",
"dev:record": "TUSK_DRIFT_MODE=RECORD node --import ./dist/tuskDriftInit.js dist/server.js"
}
}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.
Sampling controls what percentage of inbound requests are recorded in RECORD mode.
Tusk Drift supports two sampling modes in .tusk/config.yaml:
fixed: record requests at a constant base rate.adaptive: start from a base rate and automatically shed load when queue pressure, export failures, export timeouts, event loop lag, or memory pressure indicate the SDK should back off. In severe conditions the SDK can temporarily pause recording entirely.
Sampling configuration is resolved in two layers:
- Base rate precedence (highest to lowest):
TuskDrift.initialize({ samplingRate: ... })TUSK_RECORDING_SAMPLING_RATE- legacy alias
TUSK_SAMPLING_RATE .tusk/config.yamlrecording.sampling.base_rate.tusk/config.yamllegacyrecording.sampling_rate- default base rate
1.0
- Mode and minimum rate:
recording.sampling.modecomes from.tusk/config.yamland defaults tofixedrecording.sampling.min_rateis only used inadaptivemode and defaults to0.001when omitted
Note: Requests before
TuskDrift.markAppAsReady()are always recorded. Sampling applies to normal inbound traffic after startup.
Set the base sampling rate directly in your initialization code:
TuskDrift.initialize({
apiKey: process.env.TUSK_API_KEY,
env: process.env.NODE_ENV,
samplingRate: 0.1, // 10% of requests
});Set the TUSK_RECORDING_SAMPLING_RATE environment variable to override the base sampling rate:
# Development - record everything
TUSK_RECORDING_SAMPLING_RATE=1.0 npm run dev
# Production - sample 10% of requests
TUSK_RECORDING_SAMPLING_RATE=0.1 npm startTUSK_SAMPLING_RATE is still supported as a backward-compatible alias, but new setups should prefer TUSK_RECORDING_SAMPLING_RATE.
Use the nested recording.sampling config to choose fixed vs adaptive mode and set the base/minimum rates.
# ... existing configuration ...
recording:
sampling:
mode: fixed
base_rate: 0.1
export_spans: true
enable_env_var_recording: trueAdaptive sampling example:
# ... existing configuration ...
recording:
sampling:
mode: adaptive
base_rate: 0.25
min_rate: 0.01
export_spans: trueLegacy config still supported:
recording:
sampling_rate: 0.1When adaptive mode changes state or multiplier, the SDK logs an Adaptive sampling updated (...) line at info level.
state: controller state such ashealthy,warm,hot, orcritical_pausemultiplier: factor applied tobase_rateeffectiveRate: current root-request recording rate after sheddingpressure: highest normalized pressure signal (0..1) driving the updatequeueFill: smoothed export-queue usage ratio; values near1.0mean the exporter is falling behindeventLoopLagP95Ms: Node-only p95 event loop lag signalmemoryPressureRatio: current memory usage relative to its detected limit, when available
Set recording.sampling.log_transitions: false in .tusk/config.yaml, or set TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS=false, if you want to suppress these transition logs without changing the overall SDK log level. Raising logLevel to warn or higher will also hide them.
| Option | Type | Default | Description |
|---|---|---|---|
sampling.mode |
"fixed" | "adaptive" |
"fixed" |
Selects constant sampling or adaptive load shedding. |
sampling.base_rate |
number |
1.0 |
The base sampling rate (0.0 - 1.0). This is the preferred config key and can be overridden by TUSK_RECORDING_SAMPLING_RATE or the samplingRate init parameter. |
sampling.min_rate |
number |
0.001 in adaptive mode |
The minimum steady-state sampling rate for adaptive mode. In critical conditions the SDK can still temporarily pause recording. |
sampling.log_transitions |
boolean |
true |
Controls whether adaptive sampling emits Adaptive sampling updated (...) transition logs. Can be overridden by TUSK_RECORDING_SAMPLING_LOG_TRANSITIONS. |
sampling_rate |
number |
None |
Legacy fallback for the base sampling rate. Still supported for backward compatibility, but recording.sampling.base_rate is preferred. |
export_spans |
boolean |
false |
Whether to export spans to Tusk backend or local files (.tusk/traces). If false, spans are only exported to local files. |
enable_env_var_recording |
boolean |
false |
Whether to enable environment variable recording and replaying. Recommended if your application's business logic depends on environment variables, as this ensures the most accurate replay behavior. |
Once your application has completed initialization (database connections, middleware setup, etc.), mark it as ready:
// e.g. server.ts
import { TuskDrift } from "./tuskDriftInit";
// ... other imports ...
const app = express();
// Your application setup...
app.listen(8000, () => {
// Mark app as ready for recording/replay
TuskDrift.markAppAsReady();
console.log("Server started and ready for Tusk Drift");
});The SDK automatically registers ESM loader hooks via 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.
In rare cases, the wrapping can cause issues with instrumented packages:
- 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. - Native or WASM bindings: Packages with native addons loaded via ESM can conflict with the proxy wrapping mechanism.
- Bundler-generated ESM: Code that was bundled (e.g., by esbuild or webpack) into ESM sometimes produces patterns the wrapper does not handle correctly.
- Circular ESM imports: The proxy layer can interact badly with circular ESM import graphs in some edge cases.
If you encounter errors like:
SyntaxError: The requested module '...' does not provide an export named '...'
(node:1234) Error: 'import-in-the-middle' failed to wrap 'file://../../path/to/file.js'
You can disable the automatic ESM hook registration:
TuskDrift.initialize({
apiKey: process.env.TUSK_API_KEY,
env: process.env.NODE_ENV,
registerEsmLoaderHooks: false,
});Note: Disabling ESM loader hooks means the SDK will only be able to instrument packages loaded via CommonJS (
require()). Packages loaded purely through ESMimportstatements will not be intercepted. Node.js built-in modules (likehttp,https,net) are always loaded through the CJS path internally, so they will continue to be instrumented regardless of this setting.