Skip to content

Commit ef0827b

Browse files
committed
fix(otel): resolve the app's @opentelemetry/api for directory entrypoints
This fixes the v5 bridge sharing dd-trace's bundled (older) copy instead of the application's when the app is launched with a directory entrypoint (`node .`, `node path/to/app`). In that case `process.argv[1]` is the directory while `require.main.filename` is the resolved main file; rooting `createRequire` at the directory resolves from its parent and misses the app's own `node_modules/@opentelemetry/api`, reintroducing the no-op spans of #6882 for those launches. Prefer `require.main.filename`, which is always the resolved file, and fall back to `process.argv[1]` only when it is unset. Fixes: #6882
1 parent b375026 commit ef0827b

2 files changed

Lines changed: 57 additions & 2 deletions

File tree

packages/dd-trace/src/opentelemetry/api.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ let warned = false
2929
*/
3030
function applicationRequire () {
3131
const { createRequire } = require('node:module')
32-
const entrypoint = process.argv[1] || require.main?.filename
32+
// `require.main.filename` is the resolved main file; `process.argv[1]` can be a
33+
// directory (`node .`, `node path/to/app`), and `createRequire` rooted at a
34+
// directory resolves from its parent, missing the app's own node_modules.
35+
const entrypoint = require.main?.filename ?? process.argv[1]
3336
return entrypoint ? createRequire(entrypoint) : undefined
3437
}
3538

packages/dd-trace/test/opentelemetry/api.spec.js

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use strict'
22

33
const assert = require('node:assert/strict')
4+
const { execFileSync } = require('node:child_process')
45
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = require('node:fs')
56
const { tmpdir } = require('node:os')
67
const { join } = require('node:path')
78

8-
const { describe, it, after } = require('mocha')
9+
const { describe, it, before, after } = require('mocha')
910
const sinon = require('sinon')
1011
const proxyquire = require('proxyquire')
1112

@@ -143,6 +144,57 @@ describe('opentelemetry/api loader', () => {
143144
})
144145
})
145146

147+
// The v5 path roots application resolution at the entrypoint. A directory entrypoint
148+
// (`node .`, `node path/to/app`) sets process.argv[1] to the directory while
149+
// require.main.filename holds the resolved file; createRequire rooted at the directory
150+
// resolves from its parent and misses the app's own @opentelemetry/api (issue #6882).
151+
// require.main can't be faked per-module in-process, so a real child launched with each
152+
// entrypoint shape is the only place the two globals diverge as they do in production.
153+
describe('opentelemetry/api loader application entrypoint (v5)', () => {
154+
const root = mkdtempSync(join(tmpdir(), 'dd-trace-otel-entry-'))
155+
const appDir = join(root, 'app')
156+
const appEntry = join(appDir, 'index.js')
157+
const apiDir = join(appDir, 'node_modules', '@opentelemetry', 'api')
158+
159+
after(() => {
160+
rmSync(root, { recursive: true, force: true })
161+
})
162+
163+
before(() => {
164+
mkdirSync(apiDir, { recursive: true })
165+
writeFileSync(join(apiDir, 'package.json'), JSON.stringify({ name: '@opentelemetry/api', version: '1.9.0' }))
166+
writeFileSync(join(apiDir, 'index.js'), 'module.exports = { COPY: \'app\' }\n')
167+
// Forces the v5 path (DD_MAJOR mocked) so applicationRequire actually runs; the real
168+
// require.main / process.argv[1] of this child drive the resolution base under test.
169+
writeFileSync(appEntry, [
170+
'const proxyquire = require(' + JSON.stringify(require.resolve('proxyquire')) + ')',
171+
'const { load } = proxyquire(' + JSON.stringify(require.resolve('../../src/opentelemetry/api')) + ', {',
172+
' \'../../../../version\': { DD_MAJOR: 5, \'@noCallThru\': true },',
173+
'})',
174+
'try { process.stdout.write(load().COPY ?? \'no-copy\') } catch { process.stdout.write(\'threw\') }',
175+
].join('\n') + '\n')
176+
})
177+
178+
/**
179+
* Launches the fixture app with a given entrypoint argument and returns what copy of
180+
* `@opentelemetry/api` the loader resolved (`'app'` when the application's copy won).
181+
*
182+
* @param {string} entrypoint - The path passed to `node` (directory or file).
183+
* @returns {string}
184+
*/
185+
function resolvedCopy (entrypoint) {
186+
return execFileSync(process.execPath, [entrypoint], { encoding: 'utf8' })
187+
}
188+
189+
it('resolves the application copy for a directory entrypoint (node path/to/app)', () => {
190+
assert.strictEqual(resolvedCopy(appDir), 'app')
191+
})
192+
193+
it('resolves the application copy for a file entrypoint (node path/to/app/index.js)', () => {
194+
assert.strictEqual(resolvedCopy(appEntry), 'app')
195+
})
196+
})
197+
146198
// The stubbed suite above fakes node:fs, so it cannot catch a separator mismatch between
147199
// path.join (platform-native) and the version walk. This suite writes a real package tree
148200
// and lets the production node:fs/node:path run, so the walk is exercised on every platform.

0 commit comments

Comments
 (0)