Skip to content

Commit

Permalink
Add a new initializer file (#4334)
Browse files Browse the repository at this point in the history
Previously, non-programmatic tracer initialization was done via a
combination of several command-line options, depending on whether ESM
support is required, and what version of Node.js.

This has now been consolidated to a single `initialize.mjs` file, which
can be used either with the `--loader` option, or the `--import` option.
In either case, it will initialize both the tracer and the ESM loader
hook. For versions of Node.js prior to 20.6.0, the `--loader` option
must be used. For versions after that, either may be used, but
`--import` is preferred, since the `--loader` option will eventually be
deprecated, and emits a warning as such already.

All previous behavior, and related files, are preserved, so this is a
semver-minor change.

For now, docs are not included, since the current README does not even
address non-programmatic initialization.

Future work (for future PRs):
* Have `.init()` also call `register()` for the loader hook if it hasn't
  been registered, so that CLI options become unnecessary for ESM
  support in Node.js
* Allow programmatic config when using non-programmatic initialization.
  • Loading branch information
bengl authored May 23, 2024
1 parent 0a454e8 commit 3db62e3
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 34 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
!index.js
!esbuild.js
!init.js
!initialize.mjs
!loader-hook.mjs
!register.js
!package.json
Expand Down
50 changes: 50 additions & 0 deletions initialize.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* This file serves one of two purposes, depending on how it's used.
*
* If used with --import, it will import init.js and register the loader hook.
* If used with --loader, it will act as the loader hook, except that it will
* also import init.js inside the source code of the entrypoint file.
*
* The result is that no matter how this file is used, so long as it's with
* one of the two flags, the tracer will always be initialized, and the loader
* hook will always be active for ESM support.
*/

import { isMainThread } from 'worker_threads'
import { register } from 'node:module';

import { fileURLToPath } from 'node:url'
import {
load as origLoad,
resolve as origResolve,
getFormat as origGetFormat,
getSource as origGetSource
} from 'import-in-the-middle/hook.mjs'

let hasInsertedInit = false
function insertInit (result) {
if (!hasInsertedInit) {
hasInsertedInit = true
result.source = `
import '${fileURLToPath(new URL('./init.js', import.meta.url))}';
${result.source}`
}
return result
}

export async function load (...args) {
return insertInit(await origLoad(...args))
}

export const resolve = origResolve

export const getFormat = origGetFormat

export async function getSource (...args) {
return insertInit(await origGetSource(...args))
}

if (isMainThread) {
await import('./init.js')
register('./loader-hook.mjs', import.meta.url)
}
4 changes: 2 additions & 2 deletions integration-tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ function spawnProc (filename, options = {}, stdioHandler) {
stdioHandler(data)
}
// eslint-disable-next-line no-console
console.log(data.toString())
if (!options.silent) console.log(data.toString())
})

proc.stderr.on('data', data => {
// eslint-disable-next-line no-console
console.error(data.toString())
if (!options.silent) console.error(data.toString())
})
})
}
Expand Down
112 changes: 80 additions & 32 deletions integration-tests/init.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,99 @@ const path = require('path')

const DD_INJECTION_ENABLED = 'tracing'

describe('init.js', () => {
let cwd, proc, sandbox

async function runTest (cwd, env, expected) {
return new Promise((resolve, reject) => {
spawnProc(path.join(cwd, 'init/index.js'), { cwd, env }, data => {
try {
assert.strictEqual(data.toString(), expected)
resolve()
} catch (e) {
reject(e)
}
}).then(subproc => {
proc = subproc
})
})
}
let cwd, proc, sandbox

before(async () => {
sandbox = await createSandbox()
cwd = sandbox.folder
})
afterEach(() => {
proc && proc.kill()
})
after(() => {
return sandbox.remove()
async function runTest (cwd, file, env, expected) {
return new Promise((resolve, reject) => {
spawnProc(path.join(cwd, file), { cwd, env, silent: true }, data => {
try {
assert.strictEqual(data.toString(), expected)
resolve()
} catch (e) {
reject(e)
}
}).then(subproc => {
proc = subproc
})
})
}

function testInjectionScenarios (arg, filename, esmWorks = false) {
context('when dd-trace is not in the app dir', () => {
const NODE_OPTIONS = `--require ${path.join(__dirname, '..', 'init.js')}`
const NODE_OPTIONS = `--no-warnings --${arg} ${path.join(__dirname, '..', filename)}`
it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => {
return runTest(cwd, { NODE_OPTIONS }, 'true\n')
return runTest(cwd, 'init/trace.js', { NODE_OPTIONS }, 'true\n')
})
it('should not initialize the tracer, if DD_INJECTION_ENABLED', () => {
return runTest(cwd, { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'false\n')
return runTest(cwd, 'init/trace.js', { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'false\n')
})
it('should initialize instrumentation, if no DD_INJECTION_ENABLED', () => {
return runTest(cwd, 'init/instrument.js', { NODE_OPTIONS }, 'true\n')
})
it('should not initialize instrumentation, if DD_INJECTION_ENABLED', () => {
return runTest(cwd, 'init/instrument.js', { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'false\n')
})
it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation, if no DD_INJECTION_ENABLED`, () => {
return runTest(cwd, 'init/instrument.mjs', { NODE_OPTIONS }, `${esmWorks}\n`)
})
it('should not initialize ESM instrumentation, if DD_INJECTION_ENABLED', () => {
return runTest(cwd, 'init/instrument.mjs', { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'false\n')
})
})
context('when dd-trace in the app dir', () => {
const NODE_OPTIONS = '--require dd-trace/init.js'
const NODE_OPTIONS = `--no-warnings --${arg} dd-trace/${filename}`
it('should initialize the tracer, if no DD_INJECTION_ENABLED', () => {
return runTest(cwd, { NODE_OPTIONS }, 'true\n')
return runTest(cwd, 'init/trace.js', { NODE_OPTIONS }, 'true\n')
})
it('should initialize the tracer, if DD_INJECTION_ENABLED', () => {
return runTest(cwd, { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'true\n')
return runTest(cwd, 'init/trace.js', { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'true\n')
})
it('should initialize instrumentation, if no DD_INJECTION_ENABLED', () => {
return runTest(cwd, 'init/instrument.js', { NODE_OPTIONS }, 'true\n')
})
it('should initialize instrumentation, if DD_INJECTION_ENABLED', () => {
return runTest(cwd, 'init/instrument.js', { NODE_OPTIONS, DD_INJECTION_ENABLED }, 'true\n')
})
it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation, if no DD_INJECTION_ENABLED`, () => {
return runTest(cwd, 'init/instrument.mjs', { NODE_OPTIONS }, `${esmWorks}\n`)
})
it(`should ${esmWorks ? '' : 'not '}initialize ESM instrumentation, if DD_INJECTION_ENABLED`, () => {
return runTest(cwd, 'init/instrument.mjs', { NODE_OPTIONS, DD_INJECTION_ENABLED }, `${esmWorks}\n`)
})
})
}

describe('init.js', () => {
before(async () => {
sandbox = await createSandbox()
cwd = sandbox.folder
})
afterEach(() => {
proc && proc.kill()
})
after(() => {
return sandbox.remove()
})

testInjectionScenarios('require', 'init.js', false)
})

describe('initialize.mjs', () => {
before(async () => {
sandbox = await createSandbox()
cwd = sandbox.folder
})
afterEach(() => {
proc && proc.kill()
})
after(() => {
return sandbox.remove()
})

context('as --loader', () => {
testInjectionScenarios('loader', 'initialize.mjs', true)
})
context('as --import', () => {
testInjectionScenarios('import', 'initialize.mjs', true)
})
})
21 changes: 21 additions & 0 deletions integration-tests/init/instrument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const http = require('http')
const dc = require('dc-polyfill')

let gotEvent = false
dc.subscribe('apm:http:client:request:start', (event) => {
gotEvent = true
})

const server = http.createServer((req, res) => {
res.end('Hello World')
}).listen(0, () => {
http.get(`http://localhost:${server.address().port}`, (res) => {
res.on('data', () => {})
res.on('end', () => {
server.close()
// eslint-disable-next-line no-console
console.log(gotEvent)
process.exit()
})
})
})
21 changes: 21 additions & 0 deletions integration-tests/init/instrument.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import http from 'http'
import dc from 'dc-polyfill'

let gotEvent = false
dc.subscribe('apm:http:client:request:start', (event) => {
gotEvent = true
})

const server = http.createServer((req, res) => {
res.end('Hello World')
}).listen(0, () => {
http.get(`http://localhost:${server.address().port}`, (res) => {
res.on('data', () => {})
res.on('end', () => {
server.close()
// eslint-disable-next-line no-console
console.log(gotEvent)
process.exit()
})
})
})
File renamed without changes.

0 comments on commit 3db62e3

Please sign in to comment.