UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

195 lines (160 loc) 7.97 kB
/* eslint n/no-unsupported-features/node-builtins: ['error', { ignores: ['module.registerHooks'] }] */ import * as Module from 'node:module' import { pathToFileURL } from 'node:url' import { isMainThread } from 'node:worker_threads' import { createHook, supportsSyncHooks } from 'import-in-the-middle/create-hook.mjs' import { initialize as origInitialize, load as origLoad } from 'import-in-the-middle/hook.mjs' import * as rewriterLoader from './packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs' // This file must support Node.js 12.0.0 syntax const { builtinModules } = Module const require = Module.createRequire(import.meta.url) // The query marks initialize.mjs's application-thread preload; loader workers use the full graph. const isInitializeMainThread = isMainThread && import.meta.url.endsWith('?initialize') let syncImportInTheMiddleHook let getValueFromEnvSources let regexpEscape // Substrings of resolved URLs that import-in-the-middle must never wrap: re-export // shims and internal helper graphs that break when proxied, plus iitm's own files // (via `middle`). One alternation so a single test() covers every excluded load. export const iitmExclusionRegExp = /middle|langsmith|openai\/_shims|openai\/resources\/chat\/completions\/messages|openai\/agents-core\/dist\/shims|@anthropic-ai\/sdk\/_shims/ // Instrumented bare specifiers (`import 'express'`, builtins, symlinked or // workspace packages) match against the specifier (the Set); their files inside // node_modules match against the URL (the alternation). regexpEscape guards // against a regex metacharacter entering a package name. const includeModuleNames = new Set() let moduleNameAlternation = '' if (!isInitializeMainThread) { const regexpEscapeModule = require('./vendor/dist/escape-string-regexp/index.js') const hooks = require('./packages/datadog-instrumentations/src/helpers/hooks.js') const configHelper = require('./packages/dd-trace/src/config/helper.js') const { isRelativeRequire } = require('./packages/datadog-instrumentations/src/helpers/shared-utils.js') regexpEscape = regexpEscapeModule.default getValueFromEnvSources = configHelper.getValueFromEnvSources for (const moduleName of Object.keys(hooks)) { // Relative hooks resolve outside node_modules and are not instrumented here. if (isRelativeRequire(moduleName)) continue includeModuleNames.add(moduleName) // iitm matches a built-in by its node: specifier too, so mirror that and // wrap `import 'node:crypto'` as well as `import 'crypto'`. if (builtinModules.includes(moduleName)) includeModuleNames.add(`node:${moduleName}`) if (moduleNameAlternation !== '') moduleNameAlternation += '|' moduleNameAlternation += regexpEscape(moduleName) } } const nodeModulesIncludeSource = `node_modules/(?:${moduleNameAlternation})/(?!node_modules).+` function initialize (data = {}) { prepareImportInTheMiddleOptions(data) return origInitialize(data) } function prepareImportInTheMiddleOptions (data = {}) { // A consumer-owned shouldInclude predicate takes over the wrapping decision, so // iitm ignores the include/exclude arrays. Building the matcher here keeps the // synchronous and asynchronous loaders on one matching implementation. data.shouldInclude = createShouldInclude(getValueFromEnvSources('DD_IAST_SECURITY_CONTROLS_CONFIGURATION')) return data } /** * Builds the import-in-the-middle `shouldInclude(url, specifier)` predicate. iitm * calls it for every resolved module and wraps the module when the result is * truthy; supplying it replaces iitm's include/exclude list scan. * * @param {string} [securityControlsConfig] Raw `DD_IAST_SECURITY_CONTROLS_CONFIGURATION`; * each entry's module path is instrumented in addition to the hook table. */ function createShouldInclude (securityControlsConfig) { const includeRegExp = new RegExp(buildIncludeSource(securityControlsConfig)) /** * @param {string} url Resolved module URL (`file:`, `node:`, ...). * @param {string} specifier Original import specifier. */ return function shouldInclude (url, specifier) { return (includeModuleNames.has(specifier) || includeRegExp.test(url)) && !iitmExclusionRegExp.test(url) } } /** * Appends each `DD_IAST_SECURITY_CONTROLS_CONFIGURATION` module path — the third * `:`-separated segment of every `;`-separated `<type>:<marks>:<module>:<...>` entry — * to the include alternation, escaped. * * @param {string} [securityControlsConfig] Raw `DD_IAST_SECURITY_CONTROLS_CONFIGURATION`. */ function buildIncludeSource (securityControlsConfig) { if (!securityControlsConfig) return nodeModulesIncludeSource let includeSource = nodeModulesIncludeSource for (const entry of securityControlsConfig.split(';')) { if (!entry) continue const first = entry.indexOf(':') if (first === -1) continue const second = entry.indexOf(':', first + 1) if (second === -1) continue const third = entry.indexOf(':', second + 1) const subpath = entry.slice(second + 1, third === -1 ? undefined : third).trim() if (subpath) includeSource += `|${regexpEscape(subpath)}` } return includeSource } function load (url, context, nextLoad) { return rewriterLoader.load(url, context, (url, context) => origLoad(url, context, nextLoad)) } function loadSync (url, context, nextLoad) { if (isCommonJSLoad(context)) { return getSyncImportInTheMiddleHook().loadSync(url, context, nextLoad) } return rewriterLoader.loadSync(url, context, (url, context) => { return getSyncImportInTheMiddleHook().loadSync(url, context, nextLoad) }) } function isCommonJSLoad (context) { if (context.format) return context.format === 'commonjs' // Sync hooks report CommonJS require() dependency loads with a `require` // condition but no format. If a format is present, trust it instead: ESM // loaded through require() reports `format: 'module'` and still needs rewrite. const conditions = context.conditions if (!conditions) return false for (let i = 0; i < conditions.length; i++) { if (conditions[i] === 'require') return true } return false } function getSyncImportInTheMiddleHook () { if (syncImportInTheMiddleHook) { return syncImportInTheMiddleHook } const importInTheMiddleRegisterHooksUrl = pathToFileURL( require.resolve('import-in-the-middle/register-hooks.mjs') ).href syncImportInTheMiddleHook = createHook({ url: importInTheMiddleRegisterHooksUrl }) return syncImportInTheMiddleHook } function registerSyncLoaderHooks (data = {}) { // The synchronous loader strips the source of a require() pulled into the iitm // ESM graph so Node loads it natively, but module.registerHooks rejected that // nullish CommonJS source until nodejs/node#59929 (released in 22.22.3, 24.11.1, // 25.1.0 and 26.0.0). On versions that ship registerHooks but predate the fix, // fall back to the asynchronous loader instead of crashing mid-graph. if (!supportsSyncHooks()) { return false } const syncHook = getSyncImportInTheMiddleHook() if ( typeof Module.registerHooks !== 'function' || typeof syncHook.applyOptions !== 'function' || typeof syncHook.loadSync !== 'function' || typeof syncHook.resolveSync !== 'function' ) { return false } // Node built-ins are instrumented under the synchronous loader as well: iitm // reads a built-in's exports through process.getBuiltinModule(), which // bypasses the registered hooks and therefore cannot re-enter them. The // synchronous and asynchronous loaders share the same option preparation so // that `import http from 'node:http'` is wrapped on both paths. syncHook.applyOptions(prepareImportInTheMiddleOptions(data)) Module.registerHooks({ resolve: syncHook.resolveSync, load: loadSync, }) return true } export { resolve } from 'import-in-the-middle/hook.mjs' export { initialize, load, registerSyncLoaderHooks }