Skip to main content

Node.js/JavaScript SDK Integration Best Practices

Last updated 5 min read

coolhand-node patches fetch and https.request at initialization time. Import order, module system (CJS vs native ESM), and when you construct AI client objects all affect whether that patch actually intercepts your calls. This guide covers the three most common ways Node/JS integrations silently stop being monitored, and how to verify interception in CI instead of trusting a 200 response.

Coolhand's Node.js SDK (coolhand-node) monitors LLM calls by patching fetch and https.request when you call initializeGlobalMonitoring() (or load the coolhand-node/auto-monitor entry point). That's a runtime patch, not a build-time transformation — so when and how your app loads Coolhand relative to your AI SDKs determines whether monitoring actually captures anything. All three pitfalls below share the same symptom: your app works perfectly, returns correct responses, and Coolhand logs nothing — no error, no warning.

Initialize monitoring before importing AI libraries

Why it matters: Some AI client libraries reference the patched functions (https.request, fetch) as soon as their module body runs, not just when you construct a client. If Coolhand hasn't patched them yet, the library binds to the original, unpatched versions.

// ❌ Wrong order — AI library loads before monitoring patches anything
const { ChatOpenAI } = require('@langchain/openai');
const { initializeGlobalMonitoring } = require('coolhand-node');
initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
// ✅ Correct order — monitoring patches first
const { initializeGlobalMonitoring } = require('coolhand-node');
initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
const { ChatOpenAI } = require('@langchain/openai');

Native ESM projects need the --import flag

Why it matters: In a CommonJS project, require() calls run top-to-bottom, so the correct order above is enough. In a native ESM project ("type": "module" in package.json), static import statements are hoisted and evaluated before any module body runs — including a sibling import 'coolhand-node/auto-monitor' placed first in the file. The AI library's module can still capture the original https.request before Coolhand's patch applies, even though the import statements look correctly ordered in the source.

// ❌ Broken in native ESM — imports are hoisted, so source order doesn't guarantee patch order
import 'coolhand-node/auto-monitor';
import { ChatOpenAI } from '@langchain/openai'; // may still capture the original https.request
# ✅ Fix (Node ≥ 20.6) — run the patch outside your app's module graph, before anything else loads
node --import 'coolhand-node/auto-monitor' app.mjs
{ "scripts": { "start": "node --import 'coolhand-node/auto-monitor' dist/app.mjs" } }
// ✅ Fallback for Node < 20.6 — dynamic import runs after the patch is applied
import { initializeGlobalMonitoring } from 'coolhand-node';

await initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
const { ChatOpenAI } = await import('@langchain/openai'); // loads after the patch

Never construct AI clients at module scope

Why it matters: This is the subtlest of the three, because it isn't about import/require order at all. Some client libraries — including the openai SDK (v5+) — capture fetch by value, inside their constructor, instead of looking it up fresh on every request:

// Inside the library's own constructor
this.fetch = options.fetch ?? globalThis.fetch;

If you construct that client before initializeGlobalMonitoring() runs, this.fetch permanently holds the original, unpatched fetch — even though globalThis.fetch itself gets reassigned to the patched version moments later. Every call that client makes from then on silently bypasses monitoring. This bites both CommonJS and ESM projects alike, and it bites you even if your require/import order is otherwise correct, because the failure happens at construction time, not module load time. Constructing a client at module scope — a extremely common, completely reasonable-looking pattern — is enough to trigger it.

// ❌ Broken — client constructed before monitoring is initialized
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // captures unpatched fetch

await initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
// openai.chat.completions.create(...) never gets intercepted — no error, no warning
// ✅ Fixed — construct the client lazily, after monitoring is guaranteed to be initialized
import OpenAI from 'openai';

let openai;
function getOpenAIClient() {
  openai ??= new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  return openai;
}

await initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
await getOpenAIClient().chat.completions.create({ /* ... */ }); // ✅ intercepted

General rule: never construct an AI client instance at module scope in an app that relies on runtime monitoring patching. Construct clients lazily, on first use, so client construction can never race ahead of monitoring initialization — regardless of whether the client library caches fetch by value.

Verify interception in CI, not just a 200 response

A demo or example app returning a correct response proves your AI provider integration works. It does not prove Coolhand is monitoring it — all three pitfalls above produce a fully working app with silently missing logs. If you maintain an example or integration test for your Coolhand setup, assert programmatically that a request was actually intercepted (e.g. check for the log line your monitoring emits, or stub the AI provider and assert Coolhand's capture hook fired), not just that the app returned success.

Related articles