Skip to content

Commit ece16d5

Browse files
authored
fix(instrumentation): surface late-load framework warning regardless of startupLogs (#9068)
* fix(instrumentation): surface late-load framework warning regardless of startupLogs The Next.js late-load warning was gated on startupLogs, which applyV5Overrides defaults to off on v5. The users it targets run with no diagnostics enabled, so the gate hid it from exactly the people whose integration had silently no-op'd. It now flushes unconditionally through the always-on writer, independent of the startupLogs-gated startup-log diagnostics, and drops the DATADOG TRACER DIAGNOSTIC prefix that marked it as one. Refs: #5430 Refs: #5432 * fix(instrumentation): keep broad load-order warnings behind startupLogs Only the curated framework warnings (Next.js) must surface unconditionally: a silently-disabled integration has no other signal. The broad "package X was loaded before dd-trace" list is startup-diagnostic detail, gated behind startupLogs with the DATADOG TRACER DIAGNOSTIC prefix and moved to its own queue so the DD_TRACE_DEBUG-gated conflict warnings are unaffected. * fix(instrumentation): prefix the Next.js late-load warning The curated framework warning and the broad load-order list are the same kind of diagnostic, so both carry the DATADOG TRACER DIAGNOSTIC prefix. The prefix is a source marker, not a gating signal; the Next.js warning stays unconditional. * test(instrumentation): pin late-load warning behavior across startupLogs The subprocess tests only ran the warnings with startupLogs on (its v6 default), so neither the unconditional Next.js warning nor the startupLogs gate on the broad list was pinned: a regression on either would still pass on v6. Add the startupLogs=off cases for both. Drive-by fix: * Trim the load-order warning comments to the why.
1 parent 6e114b3 commit ece16d5

7 files changed

Lines changed: 85 additions & 106 deletions

File tree

packages/datadog-instrumentations/src/helpers/check-require-cache.js

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
'use strict'
22

3-
// This code runs before the tracer is configured and before a logger is ready
4-
// For that reason we queue up the messages now and decide what to do with them later
5-
const warnings = []
6-
// Same idea, but for the high-signal framework warnings that surface by default
7-
// (see flushFrameworkWarnings) rather than only under DD_TRACE_DEBUG.
8-
const frameworkWarnings = []
3+
// Queued before the logger is ready; flushed once the tracer knows its config.
4+
const warnings = [] // conflicts; flushed under DD_TRACE_DEBUG
5+
const loadOrderWarnings = [] // "loaded before dd-trace"; flushed under startupLogs
6+
const frameworkWarnings = [] // curated (e.g. Next.js); flushed unconditionally
97

108
/**
119
* Here we maintain a list of packages that an application
@@ -65,11 +63,11 @@ const earlyLoadFrameworks = new Map([
6563
* unsupported version then a warning would still be displayed.
6664
* This is OK as the tracer should be loaded earlier anyway.
6765
*
68-
* Curated frameworks (see `earlyLoadFrameworks`) are collected regardless of
69-
* `debug`, since they surface by default; the broad list stays debug-only.
70-
* @param {boolean} debug Whether to also queue the broad DD_TRACE_DEBUG-only warnings.
66+
* Curated frameworks (see `earlyLoadFrameworks`) surface unconditionally; the
67+
* broad list of packages loaded before dd-trace is queued for the startupLogs-
68+
* gated flush (`flushLoadOrderWarnings`).
7169
*/
72-
module.exports.checkForRequiredModules = function (debug) {
70+
module.exports.checkForRequiredModules = function () {
7371
const packages = require('./hooks')
7472
const naughties = new Set()
7573
const frameworksSeen = new Set()
@@ -84,8 +82,8 @@ module.exports.checkForRequiredModules = function (debug) {
8482

8583
// A curated framework loads its own server before user code, so its server
8684
// module being cached means dd-trace was too late to instrument it. These
87-
// surface by default (see flushFrameworkWarnings) with an actionable
88-
// message, so they never fall through to the DD_TRACE_DEBUG-only list below.
85+
// surface unconditionally (see flushFrameworkWarnings) with an actionable
86+
// message, so they never fall through to the broad load-order list below.
8987
const framework = earlyLoadFrameworks.get(pkg)
9088
if (framework !== undefined) {
9189
if (!frameworksSeen.has(pkg) && path?.endsWith(framework.file)) {
@@ -100,15 +98,17 @@ module.exports.checkForRequiredModules = function (debug) {
10098
continue
10199
}
102100

103-
if (!debug || naughties.has(pkg) || !(pkg in packages)) continue
101+
if (naughties.has(pkg) || !(pkg in packages)) continue
104102

105-
warnings.push(() => `Warning: Package '${pkg}' was loaded before dd-trace! This may break instrumentation.`)
103+
loadOrderWarnings.push(
104+
() => `Warning: Package '${pkg}' was loaded before dd-trace! This may break instrumentation.`
105+
)
106106

107107
naughties.add(pkg)
108108
didWarn = true
109109
}
110110

111-
if (didWarn) warnings.push('Warning: Please ensure dd-trace is loaded before other modules.')
111+
if (didWarn) loadOrderWarnings.push('Warning: Please ensure dd-trace is loaded before other modules.')
112112
}
113113

114114
/**
@@ -152,10 +152,24 @@ module.exports.flushStartupLogs = function (log) {
152152
}
153153
}
154154

155+
/**
156+
* Drains the broad "loaded before dd-trace" warnings collected by
157+
* `checkForRequiredModules`. The tracer gates this on startupLogs (these are
158+
* startup diagnostics), unlike the unconditional `flushFrameworkWarnings`.
159+
* @param {(message: string) => void} warn
160+
*/
161+
module.exports.flushLoadOrderWarnings = function (warn) {
162+
while (loadOrderWarnings.length) {
163+
const entry = loadOrderWarnings.shift()
164+
warn(typeof entry === 'function' ? entry() : entry)
165+
}
166+
}
167+
155168
/**
156169
* Drains the framework warnings collected by `checkForRequiredModules`. The
157-
* caller surfaces them by default (gated on startupLogs), unlike the
158-
* DD_TRACE_DEBUG-only `flushStartupLogs` queue.
170+
* tracer surfaces these unconditionally (not gated on startupLogs or
171+
* DD_TRACE_DEBUG), unlike the DD_TRACE_DEBUG-only `flushStartupLogs` queue,
172+
* because the affected users run with neither enabled (#5430 / #5432).
159173
* @param {(message: string) => void} warn
160174
*/
161175
module.exports.flushFrameworkWarnings = function (warn) {

packages/datadog-instrumentations/src/helpers/register.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ if (!disabledInstrumentations.has('process')) {
3838
}
3939

4040
const debugEnabled = DD_TRACE_DEBUG
41-
checkRequireCache.checkForRequiredModules(debugEnabled)
41+
checkRequireCache.checkForRequiredModules()
4242
if (debugEnabled) {
4343
setImmediate(checkRequireCache.checkForPotentialConflicts)
4444
}

packages/datadog-instrumentations/test/helpers/check-require-cache.spec.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('check-require-cache', () => {
1313
const opts = {
1414
cwd: __dirname,
1515
env: {
16-
DD_TRACE_DEBUG: 'true',
16+
DD_TRACE_STARTUP_LOGS: 'true',
1717
},
1818
}
1919

@@ -33,6 +33,15 @@ describe('check-require-cache', () => {
3333
})
3434
})
3535

36+
it('stays silent about late-loaded packages when startupLogs is off', (done) => {
37+
const off = { cwd: __dirname, env: { DD_TRACE_STARTUP_LOGS: 'false' } }
38+
exec(`${process.execPath} ./check-require-cache/bad-order.js`, off, (error, stdout, stderr) => {
39+
assert.strictEqual(error, null)
40+
assert.doesNotMatch(stderr, /Package 'express' was loaded/)
41+
done()
42+
})
43+
})
44+
3645
describe('frameworks that must load before the tracer', () => {
3746
// No DD_TRACE_DEBUG here on purpose: the framework warning has to surface by
3847
// default, since the users hitting this (issues #5430 / #5432) never turned
@@ -50,6 +59,15 @@ describe('check-require-cache', () => {
5059
})
5160
})
5261

62+
it('warns about next even when startupLogs is off (the v5 default)', (done) => {
63+
const off = { cwd: __dirname, env: { DD_TRACE_STARTUP_LOGS: 'false' } }
64+
exec(`${process.execPath} ./check-require-cache/next-loaded-first.js`, off, (error, stdout, stderr) => {
65+
assert.strictEqual(error, null)
66+
assert.match(stderr, /DATADOG TRACER DIAGNOSTIC - 'next' was loaded before dd-trace/)
67+
done()
68+
})
69+
})
70+
5371
it('should not warn when next is loaded after the tracer', (done) => {
5472
exec(`${process.execPath} ./check-require-cache/next-loaded-after.js`, defaultOpts, (error, stdout, stderr) => {
5573
assert.strictEqual(error, null)
@@ -90,8 +108,21 @@ describe('check-require-cache', () => {
90108
path.join('/app', 'node_modules', 'next', 'dist', 'server', 'next-server.js')
91109
)
92110
try {
93-
checkForRequiredModules(false)
111+
checkForRequiredModules()
112+
assert.ok(drainFrameworkWarnings().some(message => message.includes("'next' was loaded before dd-trace")))
113+
} finally {
114+
restore()
115+
}
116+
})
117+
118+
it('drains collected warnings so a second flush does not repeat them', () => {
119+
const restore = cacheModule(
120+
path.join('/app', 'node_modules', 'next', 'dist', 'server', 'next-server.js')
121+
)
122+
try {
123+
checkForRequiredModules()
94124
assert.ok(drainFrameworkWarnings().some(message => message.includes("'next' was loaded before dd-trace")))
125+
assert.deepStrictEqual(drainFrameworkWarnings(), [])
95126
} finally {
96127
restore()
97128
}
@@ -101,7 +132,7 @@ describe('check-require-cache', () => {
101132
// Literal backslash key reproduces a Windows require.cache entry on any OS.
102133
const restore = cacheModule('C:\\app\\node_modules\\next\\dist\\server\\next-server.js')
103134
try {
104-
checkForRequiredModules(false)
135+
checkForRequiredModules()
105136
assert.ok(drainFrameworkWarnings().some(message => message.includes("'next' was loaded before dd-trace")))
106137
} finally {
107138
restore()
@@ -111,12 +142,12 @@ describe('check-require-cache', () => {
111142
it('ignores non-server files of a curated framework', () => {
112143
const nextWarnings = messages => messages.filter(message => message.includes("'next'")).length
113144

114-
checkForRequiredModules(false)
145+
checkForRequiredModules()
115146
const before = nextWarnings(drainFrameworkWarnings())
116147

117148
const restore = cacheModule(path.join('/app', 'node_modules', 'next', 'package.json'))
118149
try {
119-
checkForRequiredModules(false)
150+
checkForRequiredModules()
120151
// Caching only a non-server file must not add a warning, regardless of
121152
// whatever else is already in the real require.cache.
122153
assert.strictEqual(nextWarnings(drainFrameworkWarnings()), before)
@@ -128,7 +159,7 @@ describe('check-require-cache', () => {
128159
it('does not collect packages outside the curated set', () => {
129160
const restore = cacheModule(path.join('/app', 'node_modules', 'express', 'lib', 'express.js'))
130161
try {
131-
checkForRequiredModules(false)
162+
checkForRequiredModules()
132163
assert.ok(drainFrameworkWarnings().every(message => !message.includes("'express'")))
133164
} finally {
134165
restore()

packages/dd-trace/src/proxy.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const DatadogTracer = require('./tracer')
55
const getConfig = require('./config')
66
const runtimeMetrics = require('./runtime_metrics')
77
const log = require('./log')
8-
const { setStartupLogPluginManager, startupLog, logLateLoadedFrameworks } = require('./startup-log')
8+
const { setStartupLogPluginManager, startupLog } = require('./startup-log')
99
const DynamicInstrumentation = require('./debugger')
1010
const telemetry = require('./telemetry')
1111
const nomenclature = require('./service-naming')
@@ -314,7 +314,6 @@ class Tracer extends NoopProxy {
314314
DynamicInstrumentation.configure(config)
315315
setStartupLogPluginManager(this._pluginManager)
316316
startupLog()
317-
logLateLoadedFrameworks()
318317
}
319318
}
320319

packages/dd-trace/src/startup-log.js

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const os = require('os')
44
const { inspect } = require('util')
55
const tracerVersion = require('../../../package.json').version
6-
const { flushFrameworkWarnings } = require('../../datadog-instrumentations/src/helpers/check-require-cache')
76
const { warn } = require('./log/writer')
87

98
const errors = {}
@@ -71,22 +70,6 @@ function logGenericError (message) {
7170
warn('DATADOG TRACER DIAGNOSTIC - Generic Error: ' + message)
7271
}
7372

74-
/**
75-
* Surfaces the framework warnings collected by `checkForRequiredModules` (e.g.
76-
* Next.js loaded before dd-trace, so its integration silently no-ops). Gated on
77-
* startupLogs like the other diagnostics rather than behind DD_TRACE_DEBUG,
78-
* since the affected users never have debug logging on. Draining makes repeat
79-
* calls idempotent.
80-
* @see https://github.com/DataDog/dd-trace-js/issues/5430
81-
*/
82-
function logLateLoadedFrameworks () {
83-
if (!config?.startupLogs) {
84-
return
85-
}
86-
87-
flushFrameworkWarnings(message => warn('DATADOG TRACER DIAGNOSTIC - ' + message))
88-
}
89-
9073
/**
9174
* Returns config info without integrations (used by startupLog).
9275
* @returns {Record<string, unknown>}
@@ -162,7 +145,6 @@ module.exports = {
162145
startupLog,
163146
logIntegrations,
164147
logAgentError,
165-
logLateLoadedFrameworks,
166148
setStartupLogConfig,
167149
setStartupLogPluginManager,
168150
setSamplingRules,

packages/dd-trace/src/tracer.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,22 @@
22

33
const tags = require('../../../ext/tags')
44
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants')
5-
const { flushStartupLogs } = require('../../datadog-instrumentations/src/helpers/check-require-cache')
5+
const {
6+
flushStartupLogs,
7+
flushFrameworkWarnings,
8+
flushLoadOrderWarnings,
9+
} = require('../../datadog-instrumentations/src/helpers/check-require-cache')
610
const Tracer = require('./opentracing/tracer')
711
const Scope = require('./scope')
812
const { isError } = require('./util')
913
const { setStartupLogConfig } = require('./startup-log')
1014
const { DataStreamsCheckpointer, DataStreamsManager, DataStreamsProcessor } = require('./datastreams')
1115
const { IS_SERVERLESS } = require('./serverless')
1216
const log = require('./log')
17+
// Always-on writer (console.warn), not the channel-gated `log`: these surface regardless of
18+
// DD_TRACE_DEBUG.
19+
const { warn } = require('./log/writer')
20+
const logDiagnostic = message => warn('DATADOG TRACER DIAGNOSTIC - ' + message)
1321

1422
const SPAN_TYPE = tags.SPAN_TYPE
1523
const RESOURCE_NAME = tags.RESOURCE_NAME
@@ -25,6 +33,12 @@ class DatadogTracer extends Tracer {
2533
this._scope = new Scope()
2634
setStartupLogConfig(config)
2735
flushStartupLogs(log)
36+
// Curated frameworks (e.g. Next.js) silently no-op when loaded first and their users enable
37+
// no logging (#5430 / #5432), so surface those unconditionally.
38+
flushFrameworkWarnings(logDiagnostic)
39+
if (config.startupLogs) {
40+
flushLoadOrderWarnings(logDiagnostic)
41+
}
2842

2943
if (!IS_SERVERLESS) {
3044
const storeConfig = require('./tracer_metadata')

packages/dd-trace/test/startup-log.spec.js

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -226,67 +226,6 @@ describe('startup log guards', () => {
226226
})
227227
})
228228

229-
describe('logLateLoadedFrameworks', () => {
230-
const Module = require('node:module')
231-
const path = require('node:path')
232-
const {
233-
checkForRequiredModules,
234-
flushFrameworkWarnings,
235-
} = require('../../datadog-instrumentations/src/helpers/check-require-cache')
236-
const nextServer = path.join('/app', 'node_modules', 'next', 'dist', 'server', 'next-server.js')
237-
238-
// Reproduce the detection step: cache next's server module, then run the scan
239-
// that collects the warning logLateLoadedFrameworks later surfaces.
240-
function collectNextWarning () {
241-
const fakeModule = new Module(nextServer)
242-
fakeModule.exports = {}
243-
fakeModule.loaded = true
244-
require.cache[nextServer] = fakeModule
245-
checkForRequiredModules(false)
246-
}
247-
248-
afterEach(() => {
249-
sinon.restore()
250-
delete require.cache[nextServer]
251-
flushFrameworkWarnings(() => {})
252-
})
253-
254-
it('warns when a curated framework was loaded before the tracer', () => {
255-
const warnStub = sinon.stub(console, 'warn')
256-
delete require.cache[require.resolve('../src/startup-log')]
257-
const { setStartupLogConfig, logLateLoadedFrameworks } = require('../src/startup-log')
258-
collectNextWarning()
259-
setStartupLogConfig({ startupLogs: true })
260-
logLateLoadedFrameworks()
261-
const message = warnStub.firstCall.args[0]
262-
assert.match(message, /'next' was loaded before dd-trace/)
263-
assert.match(message, /--require dd-trace\/init/)
264-
assert.match(message, /--import dd-trace\/initialize\.mjs/)
265-
assert.match(message, /serverExternalPackages/)
266-
})
267-
268-
it('does not warn when startupLogs is false', () => {
269-
const warnStub = sinon.stub(console, 'warn')
270-
delete require.cache[require.resolve('../src/startup-log')]
271-
const { setStartupLogConfig, logLateLoadedFrameworks } = require('../src/startup-log')
272-
collectNextWarning()
273-
setStartupLogConfig({ startupLogs: false })
274-
logLateLoadedFrameworks()
275-
assert.strictEqual(warnStub.callCount, 0)
276-
})
277-
278-
it('drains so a second call does not repeat the warning', () => {
279-
const warnStub = sinon.stub(console, 'warn')
280-
delete require.cache[require.resolve('../src/startup-log')]
281-
const { setStartupLogConfig, logLateLoadedFrameworks } = require('../src/startup-log')
282-
collectNextWarning()
283-
setStartupLogConfig({ startupLogs: true })
284-
logLateLoadedFrameworks()
285-
logLateLoadedFrameworks()
286-
assert.strictEqual(warnStub.callCount, 1)
287-
})
288-
})
289-
290229
describe('data_streams_enabled', () => {
291230
afterEach(() => {
292231
delete process.env.DD_DATA_STREAMS_ENABLED

0 commit comments

Comments
 (0)