diff --git a/eslint.config.mjs b/eslint.config.mjs index 6e676d732a2..2b7910595db 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -107,6 +107,37 @@ const GLOBAL_RESTRICTED_REQUIRES = [ }, ] +const SRC_RESTRICTED_SYNTAX = [ + { + // Inline `.evaluate()` callbacks (Playwright/Puppeteer) are serialized with + // `toString()` and run in chromium — coverage counters inside would ReferenceError. + selector: + "CallExpression[callee.property.name='evaluate']" + + ":matches([arguments.0.type='ArrowFunctionExpression'], [arguments.0.type='FunctionExpression'])", + message: + 'Move the inline `.evaluate(...)` callback into a `*-browser-scripts.js` file ' + + '(NYC-excluded in nyc.config.js) and import it here.', + }, + { + // Static-analysis bundlers (esbuild, webpack, rollup) only see literals as require + // arguments; once any transform (e.g. NYC) wraps them, this shape breaks bundling. + selector: "CallExpression[callee.name='require'][arguments.0.type='ConditionalExpression']", + message: 'Use `cond ? require(\'a\') : require(\'b\')` instead of `require(cond ? \'a\' : \'b\')`.', + }, +] + +// Matches only probe positions; a genuine count (`writeMapPrefix(Object.keys(x).length)`) must stay allowed. +const OBJECT_KEYS_LENGTH_PROBE = { + selector: + ':matches(BinaryExpression[right.value=0], BinaryExpression[left.value=0], UnaryExpression[operator="!"],' + + ' IfStatement, ConditionalExpression, LogicalExpression, WhileStatement, DoWhileStatement)' + + " > MemberExpression[property.name='length']" + + " > CallExpression[callee.object.name='Object'][callee.property.name='keys']", + message: 'Do not probe emptiness with `Object.keys(obj).length`; the keys array is allocated on every call. ' + + 'Track presence with a boolean at the assignment site, probe a known key (`obj.field !== undefined`), or ' + + 'return `undefined` when there is nothing to report instead of an empty object.', +} + export default [ { name: 'dd-trace/global-ignore', @@ -643,21 +674,7 @@ export default [ 'eslint-rules/eslint-prefer-set-service-name': 'error', 'eslint-rules/eslint-timer-unref': 'error', - 'no-restricted-syntax': ['error', { - // Inline `.evaluate()` callbacks (Playwright/Puppeteer) are serialized with - // `toString()` and run in chromium — coverage counters inside would ReferenceError. - selector: - "CallExpression[callee.property.name='evaluate']" + - ":matches([arguments.0.type='ArrowFunctionExpression'], [arguments.0.type='FunctionExpression'])", - message: - 'Move the inline `.evaluate(...)` callback into a `*-browser-scripts.js` file ' + - '(NYC-excluded in nyc.config.js) and import it here.', - }, { - // Static-analysis bundlers (esbuild, webpack, rollup) only see literals as require - // arguments; once any transform (e.g. NYC) wraps them, this shape breaks bundling. - selector: "CallExpression[callee.name='require'][arguments.0.type='ConditionalExpression']", - message: 'Use `cond ? require(\'a\') : require(\'b\')` instead of `require(cond ? \'a\' : \'b\')`.', - }], + 'no-restricted-syntax': ['error', ...SRC_RESTRICTED_SYNTAX], 'n/no-restricted-require': ['error', [ ...GLOBAL_RESTRICTED_REQUIRES, @@ -793,6 +810,16 @@ export default [ 'unicorn/prefer-optional-catch-binding': 'error', }, }, + { + name: 'dd-trace/packages/src', + files: [ + 'packages/*/src/**/*.js', + 'packages/*/src/**/*.mjs', + ], + rules: { + 'no-restricted-syntax': ['error', ...SRC_RESTRICTED_SYNTAX, OBJECT_KEYS_LENGTH_PROBE], + }, + }, { name: 'dd-trace/config-sync', files: [ diff --git a/packages/datadog-instrumentations/src/cucumber.js b/packages/datadog-instrumentations/src/cucumber.js index 24f923202f1..572f24b5a1b 100644 --- a/packages/datadog-instrumentations/src/cucumber.js +++ b/packages/datadog-instrumentations/src/cucumber.js @@ -116,7 +116,7 @@ let pickleByFile = {} const pickleResultByFile = {} let skippableSuites = [] -let skippableSuitesCoverage = {} +let skippableSuitesCoverage let skippedSuitesCoverage = {} let itrCorrelationId = '' let isForcedToRun = false @@ -153,12 +153,6 @@ function isValidKnownTests (receivedKnownTests) { return !!receivedKnownTests.cucumber } -function hasSkippableSuitesCoverage () { - return skippableSuitesCoverage && - typeof skippableSuitesCoverage === 'object' && - Object.keys(skippableSuitesCoverage).length > 0 -} - function isTiaCoverageBackfillEnabled () { return isItrEnabled && isCoverageReportUploadEnabled } @@ -172,7 +166,7 @@ function shouldReportCodeCoverageLinesPct (hasBackfilledCoverage) { } function getSkippedSuitesCoverageForRun () { - return isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage() + return isSuitesSkipped && isTiaCoverageBackfillEnabled() && skippableSuitesCoverage !== undefined ? skippableSuitesCoverage : {} } @@ -188,7 +182,7 @@ function getCucumberTestSessionCoverageFiles () { function resetSuiteSkippingRunState () { skippableSuites = [] - skippableSuitesCoverage = {} + skippableSuitesCoverage = undefined skippedSuitesCoverage = {} skippedSuites = [] isSuitesSkipped = false @@ -1143,7 +1137,7 @@ function getWrappedStart (start, frameworkVersion, isParallel = false, isCoordin errorSkippableRequest = skippableResponse.err skippableSuites = skippableResponse.skippableSuites ?? [] - skippableSuitesCoverage = skippableResponse.skippableSuitesCoverage ?? {} + skippableSuitesCoverage = skippableResponse.skippableSuitesCoverage if (!errorSkippableRequest) { const filteredPickles = isCoordinator diff --git a/packages/datadog-instrumentations/src/fastify.js b/packages/datadog-instrumentations/src/fastify.js index 457afb8e465..62671d3ed4b 100644 --- a/packages/datadog-instrumentations/src/fastify.js +++ b/packages/datadog-instrumentations/src/fastify.js @@ -146,6 +146,7 @@ function wrapHookDone (ctx, request, reply, req, name, doneCallback) { ctx.error = error publishError(ctx) + // eslint-disable-next-line no-restricted-syntax -- arbitrary cookie names; publishing {} sets a WAF address const hasCookies = request.cookies && Object.keys(request.cookies).length > 0 if (cookieParserReadCh.hasSubscribers && hasCookies && !cookiesPublished.has(req)) { @@ -193,6 +194,7 @@ function preHandler (request, reply, done) { const res = getRes(reply) const ctx = { req, res } + // eslint-disable-next-line no-restricted-syntax -- arbitrary body keys; publishing {} sets a WAF address const hasBody = request.body && Object.keys(request.body).length > 0 // For multipart/form-data, the body is not available until after preValidation hook diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 8db1a7b324f..e24a12003de 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -112,7 +112,7 @@ const jestSessionState = (globalThis[JEST_SESSION_STATE] ||= {}) const RETRY_TIMES = Symbol.for('RETRY_TIMES') let skippableSuites = [] -let skippableSuitesCoverage = {} +let skippableSuitesCoverage let skippedSuitesCoverage = {} let knownTests = {} let isCodeCoverageEnabled = false @@ -133,7 +133,7 @@ let isTestManagementTestsEnabled = false let testManagementTests = {} let testManagementAttemptToFixRetries = 0 let isImpactedTestsEnabled = false -let modifiedFiles = {} +let modifiedFiles let repositoryRoot let lastCoverageMap let lastCoverageMapRootDir @@ -690,8 +690,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) { if (this.isImpactedTestsEnabled) { try { - const hasImpactedTests = Object.keys(modifiedFiles).length > 0 - this.modifiedFiles = hasImpactedTests ? modifiedFiles : this.testEnvironmentOptions._ddModifiedFiles + this.modifiedFiles = modifiedFiles ?? this.testEnvironmentOptions._ddModifiedFiles } catch (e) { log.error('Error parsing impacted tests', e) this.isImpactedTestsEnabled = false @@ -2491,12 +2490,6 @@ function getRepositoryRootFromTest (test, fallbackRootDir) { return getRepositoryRootFromConfig(test?.context?.config, fallbackRootDir) } -function hasSkippableSuitesCoverage () { - return skippableSuitesCoverage && - typeof skippableSuitesCoverage === 'object' && - Object.keys(skippableSuitesCoverage).length > 0 -} - function shouldCollectJestCoverageForTia () { return shouldReportJestSuiteCoverageForTia() || (isJestCoverageBackfillSupported && isItrEnabled && isCoverageReportUploadEnabled) @@ -2607,7 +2600,7 @@ function resetLibraryConfiguration () { testManagementTests = {} testManagementAttemptToFixRetries = 0 isImpactedTestsEnabled = false - modifiedFiles = {} + modifiedFiles = undefined repositoryRoot = undefined } @@ -2629,13 +2622,14 @@ function applySuiteSkipping (originalTests, rootDir, frameworkVersion) { isSuitesSkipped ||= jestSuitesToRun.suitesToRun.length !== originalTests.length numSkippedSuites += jestSuitesToRun.skippedSuites.length - skippedSuitesCoverage = isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage() + const hasSkippableSuitesCoverage = skippableSuitesCoverage !== undefined + skippedSuitesCoverage = isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage ? skippableSuitesCoverage : {} coverageBackfillContexts = isSuitesSkipped && isTiaCoverageBackfillEnabled() ? getTestContexts(originalTests) : undefined - coverageBackfillFiles = isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage() + coverageBackfillFiles = isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage ? getCoverageBackfillFiles(skippableSuitesCoverage, repositoryRoot, getTestSuitePath) : undefined @@ -3024,10 +3018,10 @@ function getCliWrapper (isNewJestVersion) { skippableSuitesCoverage: receivedSkippableSuitesCoverage, } = skippableSuitesResponse || await getChannelPromise(skippableSuitesCh) if (err) { - skippableSuitesCoverage = {} + skippableSuitesCoverage = undefined } else { skippableSuites = receivedSkippableSuites - skippableSuitesCoverage = receivedSkippableSuitesCoverage || {} + skippableSuitesCoverage = receivedSkippableSuitesCoverage } skippedSuitesCoverage = {} } catch (err) { diff --git a/packages/datadog-instrumentations/src/mocha/main.js b/packages/datadog-instrumentations/src/mocha/main.js index ecf9082d533..312b3292763 100644 --- a/packages/datadog-instrumentations/src/mocha/main.js +++ b/packages/datadog-instrumentations/src/mocha/main.js @@ -92,7 +92,7 @@ let suitesToSkip = [] let isSuitesSkipped = false let areAllSuitesSkipped = false let skippedSuites = [] -let skippableSuitesCoverage = {} +let skippableSuitesCoverage let skippedSuitesCoverage = {} let itrCorrelationId = '' let isForcedToRun = false @@ -203,12 +203,6 @@ function getFilteredSuites (originalSuites) { }, { suitesToRun: [], skippedSuites: new Set(), suitesToSkipForRun }) } -function hasSkippableSuitesCoverage () { - return skippableSuitesCoverage && - typeof skippableSuitesCoverage === 'object' && - Object.keys(skippableSuitesCoverage).length > 0 -} - function isTiaCoverageBackfillEnabled () { return config.isItrEnabled && config.isCoverageReportUploadEnabled } @@ -239,7 +233,7 @@ function shouldReportCodeCoverageLinesPct (hasBackfilledCoverage) { } function getSkippedSuitesCoverageForRun () { - return isSuitesSkipped && isTiaCoverageBackfillEnabled() && hasSkippableSuitesCoverage() + return isSuitesSkipped && isTiaCoverageBackfillEnabled() && skippableSuitesCoverage !== undefined ? skippableSuitesCoverage : {} } @@ -257,7 +251,7 @@ function resetSuiteSkippingRunState () { isSuitesSkipped = false areAllSuitesSkipped = false skippedSuites = [] - skippableSuitesCoverage = {} + skippableSuitesCoverage = undefined skippedSuitesCoverage = {} untestedCoverage = undefined config.repositoryRoot = undefined @@ -927,11 +921,11 @@ function getExecutionConfiguration (runner, isParallel, frameworkVersion, onFini } = response || {} if (!response || err) { suitesToSkip = [] - skippableSuitesCoverage = {} + skippableSuitesCoverage = undefined } else { suitesToSkip = skippableSuites itrCorrelationId = responseItrCorrelationId - skippableSuitesCoverage = responseSkippableSuitesCoverage || {} + skippableSuitesCoverage = responseSkippableSuitesCoverage } if (localSuites) { suitesToSkip = getSuitesToSkipFromPaths(localSuites) diff --git a/packages/datadog-instrumentations/src/router.js b/packages/datadog-instrumentations/src/router.js index 00948c1e1c9..72f5023c4e8 100644 --- a/packages/datadog-instrumentations/src/router.js +++ b/packages/datadog-instrumentations/src/router.js @@ -596,6 +596,7 @@ const visitedParams = new WeakSet() function wrapHandleRequest (original) { return function wrappedHandleRequest (...args) { const req = args[0] + // eslint-disable-next-line no-restricted-syntax -- arbitrary param names; publishing {} sets a WAF address if (routerParamStartCh.hasSubscribers && !visitedParams.has(req.params) && Object.keys(req.params).length) { visitedParams.add(req.params) @@ -635,6 +636,7 @@ function wrapParam (original) { args[1] = shimmer.wrapFunction(args[1], (originalFn) => { return function wrappedFn (...fnArgs) { const req = fnArgs[0] + // eslint-disable-next-line no-restricted-syntax -- arbitrary param names; publishing {} sets a WAF address if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) { visitedParams.add(req.params) diff --git a/packages/datadog-plugin-azure-functions/src/index.js b/packages/datadog-plugin-azure-functions/src/index.js index cfbe0e5a480..522f11980d2 100644 --- a/packages/datadog-plugin-azure-functions/src/index.js +++ b/packages/datadog-plugin-azure-functions/src/index.js @@ -156,7 +156,7 @@ function setSpanLinks (triggerType, tracer, span, ctx) { : triggerMetadata.propertiesArray const addLinkFromProperties = (props) => { - if (!props || Object.keys(props).length === 0) return + if (!props) return const spanContext = tracer.extract('text_map', props) if (spanContext) { span.addLink({ context: spanContext }) diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index a2e8da17a65..13585663d58 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -477,7 +477,7 @@ class CypressPlugin { testsToSkip = [] skippedTests = [] skippedTestIds = new Set() - skippableTestsCoverage = {} + skippableTestsCoverage testSessionCoverageMap = createCoverageMap() hasForcedToRunSuites = false hasUnskippableSuites = false @@ -566,7 +566,7 @@ class CypressPlugin { this.testsToSkip = [] this.skippedTests = [] this.skippedTestIds = new Set() - this.skippableTestsCoverage = {} + this.skippableTestsCoverage = undefined this.testSessionCoverageMap = createCoverageMap() this.hasForcedToRunSuites = false this.hasUnskippableSuites = false @@ -679,17 +679,6 @@ class CypressPlugin { return this.repositoryRoot || this.rootDir || process.cwd() } - /** - * Returns whether the backend supplied skipped-test coverage data. - * - * @returns {boolean} - */ - hasSkippableTestsCoverage () { - return !!(this.skippableTestsCoverage && - typeof this.skippableTestsCoverage === 'object' && - Object.keys(this.skippableTestsCoverage).length > 0) - } - /** * Returns whether skipped test coverage should be backfilled into the session coverage map. * @@ -699,7 +688,7 @@ class CypressPlugin { return this.isItrEnabled && this.isCoverageReportUploadEnabled && this.isTestsSkipped && - this.hasSkippableTestsCoverage() + this.skippableTestsCoverage !== undefined } /** @@ -1162,7 +1151,7 @@ class CypressPlugin { } else { const { skippableTests, correlationId, skippableTestsCoverage } = skippableTestsResponse this.testsToSkip = skippableTests || [] - this.skippableTestsCoverage = skippableTestsCoverage || {} + this.skippableTestsCoverage = skippableTestsCoverage this.itrCorrelationId = correlationId incrementCountMetric(TELEMETRY_ITR_SKIPPED, { testLevel: 'test' }, this.testsToSkip.length) } diff --git a/packages/datadog-plugin-openai-agents/src/integration.js b/packages/datadog-plugin-openai-agents/src/integration.js index 79118cd2abf..b5427d3eb53 100644 --- a/packages/datadog-plugin-openai-agents/src/integration.js +++ b/packages/datadog-plugin-openai-agents/src/integration.js @@ -498,6 +498,7 @@ class OpenAIAgentsIntegration { this.#tagger.tagTextIO(ddSpan, inputValue, outputValue) + // eslint-disable-next-line no-restricted-syntax -- agents-core builds metadata before the plugin receives it if (info.metadata && Object.keys(info.metadata).length > 0) { this.#tagger.tagMetadata(ddSpan, info.metadata) } diff --git a/packages/datadog-plugin-openai/src/tracing.js b/packages/datadog-plugin-openai/src/tracing.js index 5589f6ecde4..9edbcb566de 100644 --- a/packages/datadog-plugin-openai/src/tracing.js +++ b/packages/datadog-plugin-openai/src/tracing.js @@ -102,23 +102,23 @@ class OpenAiTracingPlugin extends TracingPlugin { }, }, false) - const openaiStore = Object.create(null) - const tags = {} // The remaining tags are added one at a time if (payload.stream) { tags['openai.request.stream'] = payload.stream } + let openaiStore + switch (normalizedMethodName) { case 'createImage': case 'createImageEdit': case 'createImageVariation': - commonCreateImageRequestExtraction(tags, payload, openaiStore) + openaiStore = commonCreateImageRequestExtraction(tags, payload) break case 'createChatCompletion': - createChatCompletionRequestExtraction(tags, payload, openaiStore) + openaiStore = createChatCompletionRequestExtraction(tags, payload) break case 'createFile': @@ -128,7 +128,7 @@ class OpenAiTracingPlugin extends TracingPlugin { case 'createTranscription': case 'createTranslation': - commonCreateAudioRequestExtraction(tags, payload, openaiStore) + openaiStore = commonCreateAudioRequestExtraction(tags, payload) break case 'retrieveModel': @@ -136,11 +136,11 @@ class OpenAiTracingPlugin extends TracingPlugin { break case 'createEdit': - createEditRequestExtraction(tags, payload, openaiStore) + openaiStore = createEditRequestExtraction(tags, payload) break case 'createResponse': - createResponseRequestExtraction(tags, payload, openaiStore) + openaiStore = createResponseRequestExtraction(tags, payload) break } @@ -177,8 +177,6 @@ class OpenAiTracingPlugin extends TracingPlugin { body = coerceResponseBody(body, normalizedMethodName) - const openaiStore = store.openai - if (!error && (path?.startsWith('https://') || path?.startsWith('http://'))) { // basic checking for if the path was set as a full URL // not using a full regex as it will likely be "https://api.openai.com/..." @@ -204,7 +202,7 @@ class OpenAiTracingPlugin extends TracingPlugin { 'openai.response.created_at': body.created_at, } - responseDataExtractionByMethod(normalizedMethodName, tags, body, openaiStore) + const openaiStore = responseDataExtractionByMethod(normalizedMethodName, tags, body, store.openai) span.addTags(tags) span.finish() @@ -298,7 +296,6 @@ class OpenAiTracingPlugin extends TracingPlugin { sendLog (methodName, span, tags, openaiStore, error) { if (!openaiStore) return - if (!Object.keys(openaiStore).length) return if (!this.sampler.isSampled(span)) return const log = { @@ -395,45 +392,54 @@ function normalizeMethodName (methodName) { } } -function createEditRequestExtraction (tags, payload, openaiStore) { - const instruction = payload.instruction - openaiStore.instruction = instruction +function createEditRequestExtraction (tags, payload) { + const openaiStore = Object.create(null) + openaiStore.instruction = payload.instruction + return openaiStore } -function createResponseRequestExtraction (tags, payload, openaiStore) { +function createResponseRequestExtraction (tags, payload) { // Extract model information if (payload.model) { tags['openai.request.model'] = payload.model } // Store the full payload for response extraction + const openaiStore = Object.create(null) openaiStore.responseData = payload + return openaiStore } function retrieveModelRequestExtraction (tags, payload) { tags['openai.request.id'] = payload.id } -function createChatCompletionRequestExtraction (tags, payload, openaiStore) { +function createChatCompletionRequestExtraction (tags, payload) { const messages = payload.messages if (!defensiveArrayLength(messages)) return - openaiStore.messages = payload.messages + const openaiStore = Object.create(null) + openaiStore.messages = messages + return openaiStore } -function commonCreateImageRequestExtraction (tags, payload, openaiStore) { +function commonCreateImageRequestExtraction (tags, payload) { + let openaiStore + // createImageEdit, createImageVariation const img = payload.file || payload.image if (img !== null && typeof img === 'object' && img.path) { - const file = path.basename(img.path) - openaiStore.file = file + openaiStore = Object.create(null) + openaiStore.file = path.basename(img.path) } // createImageEdit if (payload.mask !== null && typeof payload.mask === 'object' && payload.mask.path) { - const mask = path.basename(payload.mask.path) - openaiStore.mask = mask + openaiStore ??= Object.create(null) + openaiStore.mask = path.basename(payload.mask.path) } + + return openaiStore } function responseDataExtractionByMethod (methodName, tags, body, openaiStore) { @@ -441,12 +447,10 @@ function responseDataExtractionByMethod (methodName, tags, body, openaiStore) { case 'createCompletion': case 'createChatCompletion': case 'createEdit': - commonCreateResponseExtraction(tags, body, openaiStore, methodName) - break + return commonCreateResponseExtraction(tags, body, openaiStore, methodName) case 'createResponse': - createResponseResponseExtraction(tags, body, openaiStore) - break + return createResponseResponseExtraction(tags, body, openaiStore) case 'listFiles': case 'listFineTunes': @@ -475,6 +479,8 @@ function responseDataExtractionByMethod (methodName, tags, body, openaiStore) { retrieveModelResponseExtraction(tags, body) break } + + return openaiStore } function retrieveModelResponseExtraction (tags, body) { @@ -513,10 +519,11 @@ function deleteFileResponseExtraction (tags, body) { tags['openai.response.id'] = body.id } -function commonCreateAudioRequestExtraction (tags, body, openaiStore) { +function commonCreateAudioRequestExtraction (tags, body) { if (body.file !== null && typeof body.file === 'object' && body.file.path) { - const filename = path.basename(body.file.path) - openaiStore.file = filename + const openaiStore = Object.create(null) + openaiStore.file = path.basename(body.file.path) + return openaiStore } } @@ -546,9 +553,11 @@ function commonListCountResponseExtraction (tags, body) { // createCompletion, createChatCompletion, createEdit function commonCreateResponseExtraction (tags, body, openaiStore, methodName) { - if (!body.choices) return + if (!body.choices) return openaiStore + openaiStore ??= Object.create(null) openaiStore.choices = body.choices + return openaiStore } function createResponseResponseExtraction (tags, body, openaiStore) { @@ -568,7 +577,9 @@ function createResponseResponseExtraction (tags, body, openaiStore) { } // Store the full response for potential future use + openaiStore ??= Object.create(null) openaiStore.response = body + return openaiStore } // The server almost always responds with JSON diff --git a/packages/datadog-plugin-openai/test/index.spec.js b/packages/datadog-plugin-openai/test/index.spec.js index d7e3bb4387c..b4827f33d30 100644 --- a/packages/datadog-plugin-openai/test/index.spec.js +++ b/packages/datadog-plugin-openai/test/index.spec.js @@ -160,6 +160,38 @@ describe('Plugin', () => { sinon.assert.neverCalledWith(metricStub, 'openai.ratelimit.remaining.tokens') }) + it('logs edit instructions', async function () { + if (semver.satisfies(realVersion, '>=4.0.0')) { + this.skip() + } + + const nock = require('nock') + if (!nock.isActive()) nock.activate() + + try { + const instruction = 'Fix the spelling mistakes.' + const scope = nock('http://127.0.0.1:9126') + .post('/vcr/openai/edits') + .reply(200, { + choices: [{ index: 0, text: 'What day of the week is it?' }], + }) + + const checkTrace = agent.assertFirstTraceSpan({ error: 0 }) + await openai.createEdit({ + input: 'What day of the wek is it?', + instruction, + model: 'text-davinci-edit-001', + }) + await checkTrace + + scope.done() + sinon.assert.calledWithMatch(externalLoggerStub, { instruction }) + } finally { + nock.cleanAll() + nock.restore() + } + }) + describe('maintains context', () => { it('should maintain the context with a non-streamed call', async () => { await tracer.trace('outer', async (outerSpan) => { diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js index 11ed2b2eb8b..bd7434d41eb 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js @@ -92,6 +92,7 @@ function removeEmptyCaptureProperties (value) { if (current.fields === null || ( current.fields && typeof current.fields === 'object' && + // eslint-disable-next-line no-restricted-syntax -- snapshot field names are user variables; no key to probe Object.keys(current.fields).length === 0 )) { delete current.fields diff --git a/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js b/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js index 82a6515eb16..79db5f842eb 100644 --- a/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +++ b/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js @@ -34,10 +34,11 @@ function parseSkippableSuitesResponse ( if (validateRequiredFields) { validateSkippableTestsResponse(parsedResponse, { validationMode }) } - const coverage = {} + let coverage const coverageByFilename = parsedResponse.meta?.coverage if (coverageByFilename) { for (const [filename, bitmap] of Object.entries(coverageByFilename)) { + coverage ??= {} coverage[filename.replaceAll('\\', '/')] = bitmap } } diff --git a/packages/dd-trace/src/llmobs/experiments/experiment.js b/packages/dd-trace/src/llmobs/experiments/experiment.js index ccac6252926..c08dab577c8 100644 --- a/packages/dd-trace/src/llmobs/experiments/experiment.js +++ b/packages/dd-trace/src/llmobs/experiments/experiment.js @@ -374,6 +374,7 @@ class Experiment { } const datasetVersion = this.#dataset.version() if (datasetVersion !== null) attributes.dataset_version = datasetVersion + // eslint-disable-next-line no-restricted-syntax -- faster than tracking entries while copying arbitrary config if (Object.keys(this.#config).length > 0) attributes.config = this.#config let created diff --git a/packages/dd-trace/src/llmobs/plugins/ai/util.js b/packages/dd-trace/src/llmobs/plugins/ai/util.js index 393574c8014..6e9308af5ae 100644 --- a/packages/dd-trace/src/llmobs/plugins/ai/util.js +++ b/packages/dd-trace/src/llmobs/plugins/ai/util.js @@ -228,6 +228,7 @@ function getJsonStringValue (str, defaultValue) { function getModelMetadata (tags) { /** @type {Record} */ const modelMetadata = {} + let hasModelMetadata = false for (const tag of Object.keys(tags)) { const isModelMetadata = tag.startsWith(VERCEL_AI_MODEL_METADATA_PREFIX) if (isModelMetadata) { @@ -235,6 +236,7 @@ function getModelMetadata (tags) { const metadataKey = lastCommaPosition === -1 ? tag : tag.slice(lastCommaPosition + 1) if (metadataKey && MODEL_METADATA_KEYS.has(metadataKey)) { modelMetadata[metadataKey] = tags[tag] + hasModelMetadata = true } } else { const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX) @@ -242,12 +244,13 @@ function getModelMetadata (tags) { const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length) if (metadataKey) { modelMetadata[metadataKey] = tags[tag] + hasModelMetadata = true } } } } - return Object.keys(modelMetadata).length ? modelMetadata : null + return hasModelMetadata ? modelMetadata : null } /** @@ -259,6 +262,7 @@ function getModelMetadata (tags) { function getGenerationMetadata (tags) { /** @type {Record} */ const metadata = {} + let hasMetadata = false for (const tag of Object.keys(tags)) { const isGenerationMetadata = tag.startsWith(VERCEL_AI_GENERATION_METADATA_PREFIX) @@ -270,18 +274,20 @@ function getGenerationMetadata (tags) { const settingValue = tags[tag] metadata[settingKey] = settingValue + hasMetadata = true } else { const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX) if (isTelemetryMetadata) { const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length) if (metadataKey) { metadata[metadataKey] = tags[tag] + hasMetadata = true } } } } - return Object.keys(metadata).length ? metadata : null + return hasMetadata ? metadata : null } /** @@ -307,6 +313,7 @@ function getGenerationMetadataFromEvent (event) { metadata[transformedKey] = value } + // eslint-disable-next-line no-restricted-syntax -- manual tracking would duplicate Object.assign semantics return Object.keys(metadata).length ? metadata : null } @@ -432,6 +439,7 @@ function getLlmObsSpanName (operation, functionId) { */ function getTelemetryMetadata (tags) { const metadata = {} + let hasMetadata = false for (const tag of Object.keys(tags)) { if (!tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)) continue @@ -439,10 +447,11 @@ function getTelemetryMetadata (tags) { const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length) if (metadataKey) { metadata[metadataKey] = tags[tag] + hasMetadata = true } } - return Object.keys(metadata).length ? metadata : null + return hasMetadata ? metadata : null } module.exports = { diff --git a/packages/dd-trace/src/opentelemetry/context_manager.js b/packages/dd-trace/src/opentelemetry/context_manager.js index dfbd07f49b9..bb4dcaebbe2 100644 --- a/packages/dd-trace/src/opentelemetry/context_manager.js +++ b/packages/dd-trace/src/opentelemetry/context_manager.js @@ -21,14 +21,14 @@ class ContextManager { const storedSpan = store ? trace.getSpan(store) : null // Convert DD baggage to OTel format - const baggages = getAllBaggageItems() - const hasBaggage = Object.keys(baggages).length > 0 + let entries + for (const [key, value] of Object.entries(getAllBaggageItems())) { + entries ??= {} + entries[key] = { value } + } + let otelBaggages - if (hasBaggage) { - const entries = {} - for (const [key, value] of Object.entries(baggages)) { - entries[key] = { value } - } + if (entries !== undefined) { otelBaggages = propagation.createBaggage(entries) } diff --git a/packages/dd-trace/src/plugins/util/jest.js b/packages/dd-trace/src/plugins/util/jest.js index e65fb540cbd..8c9de67e360 100644 --- a/packages/dd-trace/src/plugins/util/jest.js +++ b/packages/dd-trace/src/plugins/util/jest.js @@ -68,6 +68,8 @@ function getJestTestName (test) { function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRootDir) { const unskippableSuites = {} const forcedToRunSuites = {} + let hasUnskippableSuites = false + let hasForcedToRunSuites = false const skippedSuites = [] const suitesToRun = [] @@ -87,11 +89,13 @@ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRo if (isMarkedAsUnskippable(test)) { suitesToRun.push(test) unskippableSuites[relativePath] = true + hasUnskippableSuites = true if (fallbackRelativePath !== undefined) { unskippableSuites[fallbackRelativePath] = true } if (skippedSuite !== undefined) { forcedToRunSuites[relativePath] = true + hasForcedToRunSuites = true if (fallbackRelativePath !== undefined) { forcedToRunSuites[fallbackRelativePath] = true } @@ -105,9 +109,6 @@ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRo } } - const hasUnskippableSuites = Object.keys(unskippableSuites).length > 0 - const hasForcedToRunSuites = Object.keys(forcedToRunSuites).length > 0 - if (originalTests.length) { // The config object is shared by all tests, so we can just take the first one const [test] = originalTests diff --git a/packages/dd-trace/src/plugins/util/test.js b/packages/dd-trace/src/plugins/util/test.js index f3d57b722f4..6778fe2c77a 100644 --- a/packages/dd-trace/src/plugins/util/test.js +++ b/packages/dd-trace/src/plugins/util/test.js @@ -1439,10 +1439,6 @@ function addSkippedCoverageToMap (skippedCoverage, targetMap) { } } -function hasSkippedCoverage (skippedCoverage) { - return skippedCoverage && typeof skippedCoverage === 'object' && Object.keys(skippedCoverage).length > 0 -} - function getTestCoverageLinesPercentage (coverage, skippedCoverage, rootDir) { const executableLinesByFile = new Map() const coveredLinesByFile = new Map() @@ -1496,10 +1492,10 @@ function applySkippedCoverageToFileCoverage (fileCoverage, skippedBitmap) { * @returns {boolean} */ function applySkippedCoverageToCoverage (coverage, skippedCoverage, rootDir) { - if (!hasSkippedCoverage(skippedCoverage)) return false + const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage) + if (skippedCoverageByFilename.size === 0) return false const coverageMap = getCoverageMap(coverage) - const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage) let matched = false for (const filename of coverageMap.files()) { @@ -1820,6 +1816,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) { } const metrics = {} + let hasMetrics = false for (const candidate of candidateBranches) { // Find common ancestor const baseSha = getMergeBase(candidate, sourceBranch) @@ -1838,6 +1835,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) { ahead, baseSha, } + hasMetrics = true } function isDefaultBranch (branch) { @@ -1846,7 +1844,7 @@ function getPullRequestBaseBranch (pullRequestBaseBranch) { ) } - if (Object.keys(metrics).length === 0) { + if (!hasMetrics) { return null } // Find branch with smallest "ahead" value, preferring default branch on tie @@ -1872,6 +1870,7 @@ function getPullRequestDiff (baseCommit, targetCommit) { function getModifiedFilesFromDiff (diff) { if (!diff) return null const result = {} + let hasModifiedFiles = false const filesRegex = /^diff --git a\/(?.+) b\/(?.+)$/g const linesRegex = /^@@ -\d+(,\d+)? \+(?\d+)(,(?\d+))? @@/g @@ -1886,6 +1885,7 @@ function getModifiedFilesFromDiff (diff) { if (fileMatch && fileMatch.groups.file) { currentFile = fileMatch.groups.file result[currentFile] = [] + hasModifiedFiles = true continue } @@ -1904,7 +1904,7 @@ function getModifiedFilesFromDiff (diff) { linesRegex.lastIndex = 0 } - if (Object.keys(result).length === 0) { + if (!hasModifiedFiles) { return null } return result diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index e071cd93148..e30d002e460 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -35,7 +35,7 @@ function profileHasMissingSourceMaps (profile) { } function processInfo (infos, info, type) { - if (Object.keys(info).length > 0) { + if (info !== undefined) { infos[type] = info } } diff --git a/packages/dd-trace/src/profiling/profilers/space.js b/packages/dd-trace/src/profiling/profilers/space.js index a5c8ba25641..cfc7f54b714 100644 --- a/packages/dd-trace/src/profiling/profilers/space.js +++ b/packages/dd-trace/src/profiling/profilers/space.js @@ -66,9 +66,7 @@ class NativeSpaceProfiler { return profile } - getInfo () { - return {} - } + getInfo () {} encode (profile) { return encodeProfileAsync(profile) diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index d5f7f53c0bd..2228f853977 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -417,7 +417,7 @@ class NativeWallProfiler { if (rootSpanId !== undefined) { labels[LOCAL_ROOT_SPAN_ID_LABEL] = toBigInt(rootSpanId) } - if (webTags !== undefined && Object.keys(webTags).length !== 0) { + if (webTags !== undefined) { labels[TRACE_ENDPOINT_LABEL] = endpointNameFromTags(webTags) } else if (endpoint) { // fallback to endpoint computed when sample was taken diff --git a/packages/dd-trace/src/ritm.js b/packages/dd-trace/src/ritm.js index 88a7f09d95e..835ce0412a2 100644 --- a/packages/dd-trace/src/ritm.js +++ b/packages/dd-trace/src/ritm.js @@ -20,6 +20,7 @@ const origRequire = Module.prototype.require module.exports = Hook let moduleHooks = Object.create(null) +let hookedModuleCount = 0 let cache = Object.create(null) let patching = Object.create(null) let patchedRequire = null @@ -64,6 +65,7 @@ function Hook (modules, options, onrequire) { hooks.push(onrequire) } else { moduleHooks[mod] = [onrequire] + hookedModuleCount++ } } } @@ -205,6 +207,7 @@ Hook.reset = function () { patching = Object.create(null) cache = Object.create(null) moduleHooks = Object.create(null) + hookedModuleCount = 0 } function findProjectRoot (startDir) { @@ -221,16 +224,20 @@ function findProjectRoot (startDir) { Hook.prototype.unhook = function () { for (const mod of this.modules) { - const hooks = (moduleHooks[mod] || []).filter(hook => hook !== this.onrequire) + const registeredHooks = moduleHooks[mod] + if (registeredHooks === undefined) continue + + const hooks = registeredHooks.filter(hook => hook !== this.onrequire) if (hooks.length > 0) { moduleHooks[mod] = hooks } else { delete moduleHooks[mod] + hookedModuleCount-- } } - if (Object.keys(moduleHooks).length === 0) { + if (hookedModuleCount === 0) { Hook.reset() } } diff --git a/packages/dd-trace/test/aiguard/index.spec.js b/packages/dd-trace/test/aiguard/index.spec.js index 797c0955920..2f9af1f51c7 100644 --- a/packages/dd-trace/test/aiguard/index.spec.js +++ b/packages/dd-trace/test/aiguard/index.spec.js @@ -8,9 +8,11 @@ const msgpack = require('@msgpack/msgpack') const { afterEach, beforeEach, describe, it } = require('mocha') const sinon = require('sinon') +const { storage } = require('../../../datadog-core') const aiguardAutoInstrumentation = require('../../src/aiguard') const NoopAIGuard = require('../../src/aiguard/noop') const AIGuard = require('../../src/aiguard/sdk') +const { withRequest } = require('../../src/appsec/store') const agent = require('../plugins/agent') const { assertObjectContains } = require('../../../../integration-tests/helpers') @@ -18,6 +20,7 @@ const tracerVersion = require('../../../../package.json').version const telemetryMetrics = require('../../src/telemetry/metrics') const aiguardMetrics = telemetryMetrics.manager.namespace('ai_guard') const { USER_KEEP } = require('../../../../ext/priority') +const { HTTP_CLIENT_IP, NETWORK_CLIENT_IP } = require('../../../../ext/tags') const { SAMPLING_MECHANISM_AI_GUARD, DECISION_MAKER_KEY } = require('../../src/constants') const { EVENT_TAG_KEY, @@ -501,6 +504,28 @@ describe('AIGuard SDK', () => { }) }) + it('copies the client ip of the active request onto the root span', async () => { + mockFetch({ + body: { data: { attributes: { action: 'ALLOW', reason: 'OK', is_blocking_enabled: false } } }, + }) + + const req = { headers: { 'x-forwarded-for': '8.8.8.8' }, socket: { remoteAddress: '10.0.0.1' } } + await tracer.trace('root', async () => { + const legacyStorage = storage('legacy') + await legacyStorage.run(withRequest(legacyStorage.getStore(), req), () => + aiguard.evaluate(prompt, { block: false }) + ) + }) + + await agent.assertSomeTraces(traces => { + const rootSpan = traces[0].find(span => span.name === 'root') + assertObjectContains(rootSpan.meta, { + [HTTP_CLIENT_IP]: '8.8.8.8', + [NETWORK_CLIENT_IP]: '10.0.0.1', + }) + }) + }) + it('parents the ai_guard span under the explicit childOf span', async () => { mockFetch({ body: { data: { attributes: { action: 'ALLOW', reason: 'OK', is_blocking_enabled: false } } }, diff --git a/packages/dd-trace/test/ci-visibility/intelligent-test-runner/get-skippable-suites.spec.js b/packages/dd-trace/test/ci-visibility/intelligent-test-runner/get-skippable-suites.spec.js index 39b615a8ef4..1dffbe91123 100644 --- a/packages/dd-trace/test/ci-visibility/intelligent-test-runner/get-skippable-suites.spec.js +++ b/packages/dd-trace/test/ci-visibility/intelligent-test-runner/get-skippable-suites.spec.js @@ -208,7 +208,7 @@ describe('get-skippable-suites', () => { assert.strictEqual(err, null) assert.deepStrictEqual(skippableSuites, ['suite1.spec.js', 'suite2.spec.js']) assert.strictEqual(correlationId, 'corr-123') - assert.deepStrictEqual(coverage, {}) + assert.strictEqual(coverage, undefined) done() }) }) @@ -425,12 +425,20 @@ describe('parseSkippableSuitesResponse', () => { assert.deepStrictEqual(result, { skippableSuites: [{ suite: 'suite1.spec.js', name: 'test 1' }], correlationId: 'corr-123', - coverage: {}, + coverage: undefined, numReceivedSkippableItems: 1, numExcludedByMissingLineCoverage: 0, }) }) + it('leaves coverage undefined when the response carries none', () => { + const withoutCoverage = parseSkippableSuitesResponse(JSON.stringify({ data: [], meta: {} })) + const withEmptyCoverage = parseSkippableSuitesResponse(JSON.stringify({ data: [], meta: { coverage: {} } })) + + assert.strictEqual(withoutCoverage.coverage, undefined) + assert.strictEqual(withEmptyCoverage.coverage, undefined) + }) + it('filters missing line coverage when coverage report upload is enabled', () => { const result = parseSkippableSuitesResponse(JSON.stringify(SKIPPABLE_RESPONSE_WITH_MISSING_LINE_COVERAGE), { testLevel: 'suite', diff --git a/packages/dd-trace/test/llmobs/experiments/experiment.spec.js b/packages/dd-trace/test/llmobs/experiments/experiment.spec.js index 170e24b51d1..4de22a29db2 100644 --- a/packages/dd-trace/test/llmobs/experiments/experiment.spec.js +++ b/packages/dd-trace/test/llmobs/experiments/experiment.spec.js @@ -53,7 +53,7 @@ function clientWithMockBackend ({ createDatasetError } = {}) { describe('LLMObs Experiments — dataset + experiment run', () => { it('runs task inside an LLMObs experiment span', async () => { - const { client: c } = clientWithMockBackend() + const { client: c, requests } = clientWithMockBackend() const dataset = new Dataset(c, 'demo').addRecord({ q: 'apple' }, 'apple', { row: 0 }) const callsToLlmobs = [] const llmobs = { @@ -72,6 +72,7 @@ describe('LLMObs Experiments — dataset + experiment run', () => { dataset, task: (input) => input.q, evaluators: { ok: () => true }, + config: { temperature: 0 }, }, llmobs).run() assert.equal(callsToLlmobs[0][0], 'trace') @@ -81,6 +82,9 @@ describe('LLMObs Experiments — dataset + experiment run', () => { assert.equal(callsToLlmobs[1][1].tags.dataset_record_id, dataset.records()[0].id) assert.equal(result.rows[0].spanId, '000000000000abcd') assert.equal(result.rows[0].traceId, '0000000000000000000000000000abcd') + assert.deepEqual(requests.find(request => request.method === 'createExperiment').attributes.config, { + temperature: 0, + }) }) it('surfaces backend failures', async () => { diff --git a/packages/dd-trace/test/plugins/util/test.spec.js b/packages/dd-trace/test/plugins/util/test.spec.js index 3cd3b6d7170..9a9189f9ee5 100644 --- a/packages/dd-trace/test/plugins/util/test.spec.js +++ b/packages/dd-trace/test/plugins/util/test.spec.js @@ -1819,6 +1819,7 @@ index 1234567..89abcde 100644 assert.strictEqual(getModifiedFilesFromDiff(''), null) assert.strictEqual(getModifiedFilesFromDiff(null), null) assert.strictEqual(getModifiedFilesFromDiff(undefined), null) + assert.strictEqual(getModifiedFilesFromDiff('not a diff\n@@ -1 +1 @@\n'), null) }) it('should handle multiple line changes in a single hunk', () => { @@ -1965,6 +1966,21 @@ describe('getPullRequestBaseBranch', () => { sinon.assert.calledWith(getCountsStub, 'master', 'feature-branch') sinon.assert.calledWith(getCountsStub, 'trunk', 'feature-branch') }) + + it('returns null when no candidate branch has a merge base', () => { + const { getPullRequestBaseBranch } = proxyquire('../../../src/plugins/util/test', { + './git': { + getGitRemoteName: () => 'origin', + getSourceBranch: () => 'feature-branch', + getMergeBase: sinon.stub().returns(undefined), + checkAndFetchBranch: sinon.stub(), + getLocalBranches: sinon.stub().returns(['trunk', 'master', 'feature-branch']), + getCounts: sinon.stub().returns({ ahead: 0, behind: 0 }), + }, + }) + + assert.strictEqual(getPullRequestBaseBranch(), null) + }) }) }) diff --git a/packages/dd-trace/test/profiling/profilers/space.spec.js b/packages/dd-trace/test/profiling/profilers/space.spec.js index 2a8574bc6fa..b0e490d4fbf 100644 --- a/packages/dd-trace/test/profiling/profilers/space.spec.js +++ b/packages/dd-trace/test/profiling/profilers/space.spec.js @@ -99,11 +99,10 @@ describe('profilers/native/space', () => { sinon.assert.calledOnce(pprof.heap.stop) }) - it('should provide info', () => { + it('should not provide info', () => { const profiler = makeSpace(NativeSpaceProfiler) - const info = profiler.getInfo() - assert.strictEqual(Object.keys(info).length, 0) + assert.strictEqual(profiler.getInfo(), undefined) }) it('should collect profiles from the pprof space profiler', () => { diff --git a/packages/dd-trace/test/ritm-tests/module-partially-unhooked.js b/packages/dd-trace/test/ritm-tests/module-partially-unhooked.js new file mode 100644 index 00000000000..dece2626b19 --- /dev/null +++ b/packages/dd-trace/test/ritm-tests/module-partially-unhooked.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports.foo = 'foo' diff --git a/packages/dd-trace/test/ritm-tests/module-sibling.js b/packages/dd-trace/test/ritm-tests/module-sibling.js new file mode 100644 index 00000000000..dece2626b19 --- /dev/null +++ b/packages/dd-trace/test/ritm-tests/module-sibling.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports.foo = 'foo' diff --git a/packages/dd-trace/test/ritm-tests/module-unhooked.js b/packages/dd-trace/test/ritm-tests/module-unhooked.js new file mode 100644 index 00000000000..dece2626b19 --- /dev/null +++ b/packages/dd-trace/test/ritm-tests/module-unhooked.js @@ -0,0 +1,3 @@ +'use strict' + +module.exports.foo = 'foo' diff --git a/packages/dd-trace/test/ritm.spec.js b/packages/dd-trace/test/ritm.spec.js index f20e0b8e670..5cff882de4b 100644 --- a/packages/dd-trace/test/ritm.spec.js +++ b/packages/dd-trace/test/ritm.spec.js @@ -149,4 +149,37 @@ describe('Ritm', () => { } } }) + + describe('unhook', () => { + /** @param {Record} exports */ + function markPatched (exports) { + exports.patched = true + return exports + } + + it('removes the hook and leaves unrelated registrations intact', () => { + Hook(['./ritm-tests/module-sibling'], markPatched) + const hook = Hook(['./ritm-tests/module-unhooked'], markPatched) + + hook.unhook() + hook.unhook() + + assert.equal(require('./ritm-tests/module-unhooked').patched, undefined) + assert.equal(require('./ritm-tests/module-sibling').patched, true) + }) + + it('keeps the hooks a module still has registered', () => { + const hook = Hook(['./ritm-tests/module-partially-unhooked'], markPatched) + Hook(['./ritm-tests/module-partially-unhooked'], (exports) => { + exports.kept = true + return exports + }) + + hook.unhook() + + const patchedModule = require('./ritm-tests/module-partially-unhooked') + assert.equal(patchedModule.patched, undefined) + assert.equal(patchedModule.kept, true) + }) + }) })