Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
748e99f
feat(telemetry): add stable session identifier headers
khanayan123 Mar 19, 2026
1c7b1a2
fix: address CI lint, test, and label failures
khanayan123 Mar 19, 2026
bd88368
refactor: fold child_session into existing child_process instrumentation
khanayan123 Mar 19, 2026
3a68e9b
refactor: rename child_session to session-propagation, gate on teleme…
khanayan123 Mar 19, 2026
a21798d
refactor: move session-propagation to top-level require
khanayan123 Mar 19, 2026
28cfefc
test: add fork and callArgs mutation tests to child_process instrumen…
khanayan123 Mar 19, 2026
651b556
fix: address CI lint and Windows test failures, clean up redundant code
khanayan123 Mar 19, 2026
6073719
chore: remove redundant comments and fix stray division operator
khanayan123 Mar 19, 2026
303e977
chore: remove duplicate tests covering same code paths
khanayan123 Mar 19, 2026
599733f
fix: resolve lint errors in test files
khanayan123 Mar 19, 2026
6a4b898
fix: restore original comments removed by simplifier
khanayan123 Mar 19, 2026
d3dd247
refactor: use switch statement in onChildProcessStart
khanayan123 Mar 19, 2026
5634511
fix: improve codecov coverage for session-propagation
khanayan123 Mar 19, 2026
09bad69
fix: preserve callbacks when injecting session env vars
khanayan123 Mar 19, 2026
281ecf0
refactor: simplify session-propagation with findOptionsIndex
khanayan123 Mar 19, 2026
626781b
chore: remove unnecessary comments from session-propagation
khanayan123 Mar 19, 2026
ca5349a
feat: add stop() lifecycle to session-propagation
khanayan123 Mar 19, 2026
9a8f4dd
fix: handle spawn(cmd, undefined, opts) in findOptionsIndex
khanayan123 Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions packages/datadog-instrumentations/src/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const {
const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')

// ignored exec method because it calls to execFile directly
const execAsyncMethods = ['execFile', 'spawn']
const execAsyncMethods = ['execFile', 'spawn', 'fork']

const names = ['child_process', 'node:child_process']

Expand Down Expand Up @@ -97,8 +97,10 @@ function wrapChildProcessSyncMethod (returnError, shell = false) {
return childProcessMethod.apply(this, arguments)
}

const childProcessInfo = normalizeArgs(arguments, shell)
const callArgs = [...arguments]
const childProcessInfo = normalizeArgs(callArgs, shell)
const context = createContextFromChildProcessInfo(childProcessInfo)
context.callArgs = callArgs

return childProcessChannel.start.runStores(context, () => {
try {
Expand All @@ -108,7 +110,7 @@ function wrapChildProcessSyncMethod (returnError, shell = false) {
return returnError(error, context)
}

const result = childProcessMethod.apply(this, arguments)
const result = childProcessMethod.apply(this, context.callArgs)
context.result = result

return result
Expand All @@ -131,9 +133,11 @@ function wrapChildProcessCustomPromisifyMethod (customPromisifyMethod, shell) {
return customPromisifyMethod.apply(this, arguments)
}

const childProcessInfo = normalizeArgs(arguments, shell)
const callArgs = [...arguments]
const childProcessInfo = normalizeArgs(callArgs, shell)

const context = createContextFromChildProcessInfo(childProcessInfo)
context.callArgs = callArgs

const { start, end, asyncStart, asyncEnd, error } = childProcessChannel
start.publish(context)
Expand All @@ -143,7 +147,7 @@ function wrapChildProcessCustomPromisifyMethod (customPromisifyMethod, shell) {
result = Promise.reject(context.abortController.signal.reason || new Error('Aborted'))
} else {
try {
result = customPromisifyMethod.apply(this, arguments)
result = customPromisifyMethod.apply(this, context.callArgs)
} catch (error) {
context.error = error
error.publish(context)
Expand Down Expand Up @@ -181,9 +185,11 @@ function wrapChildProcessAsyncMethod (ChildProcess, shell = false) {
return childProcessMethod.apply(this, arguments)
}

const childProcessInfo = normalizeArgs(arguments, shell)
const callArgs = [...arguments]
const childProcessInfo = normalizeArgs(callArgs, shell)

const context = createContextFromChildProcessInfo(childProcessInfo)
context.callArgs = callArgs
return childProcessChannel.start.runStores(context, () => {
let childProcess
if (context.abortController.signal.aborted) {
Expand All @@ -194,15 +200,15 @@ function wrapChildProcessAsyncMethod (ChildProcess, shell = false) {
const error = context.abortController.signal.reason || new Error('Aborted')
childProcess.emit('error', error)

const cb = arguments[arguments.length - 1]
const cb = context.callArgs[context.callArgs.length - 1]
if (typeof cb === 'function') {
cb(error)
}

childProcess.emit('close')
})
} else {
childProcess = childProcessMethod.apply(this, arguments)
childProcess = childProcessMethod.apply(this, context.callArgs)
}

if (childProcess) {
Expand Down
128 changes: 127 additions & 1 deletion packages/datadog-instrumentations/test/child_process.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use strict'

const assert = require('node:assert/strict')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { promisify } = require('node:util')

const dc = require('dc-polyfill')
Expand Down Expand Up @@ -326,7 +329,6 @@ describe('child process', () => {
abortController: sinon.match.instanceOf(AbortController),
shell: true,
})
sinon.assert.calledOnce(start)
sinon.assert.calledWithMatch(asyncFinish, {
command: 'echo',
file: 'echo',
Expand Down Expand Up @@ -701,6 +703,130 @@ describe('child process', () => {
})
})
})

describe('fork', () => {
let tmpScript

before(() => {
tmpScript = path.join(os.tmpdir(), `dd-trace-test-fork-${Date.now()}.js`)
fs.writeFileSync(tmpScript, 'process.exit(0)')
})

after(() => {
try { fs.unlinkSync(tmpScript) } catch (e) { /* ignore */ }
})

it('should execute success callbacks', (done) => {
const child = childProcess.fork(tmpScript)

child.once('close', () => {
sinon.assert.calledOnce(start)
sinon.assert.calledWithMatch(start, {
command: tmpScript,
file: tmpScript,
shell: false,
abortController: sinon.match.instanceOf(AbortController),
})
sinon.assert.calledOnce(asyncFinish)
sinon.assert.calledWithMatch(asyncFinish, {
command: tmpScript,
file: tmpScript,
shell: false,
result: 0,
})
sinon.assert.notCalled(error)
done()
})
})

it('should publish arguments', (done) => {
const child = childProcess.fork(tmpScript, ['--flag'])

child.once('close', () => {
sinon.assert.calledOnce(start)
sinon.assert.calledWithMatch(start, {
command: `${tmpScript} --flag`,
file: tmpScript,
fileArgs: ['--flag'],
shell: false,
abortController: sinon.match.instanceOf(AbortController),
})
done()
})
})

it('should execute error callback for non-existent module', (done) => {
const child = childProcess.fork('non_existent_module_test.js')

child.once('error', () => {})

child.once('close', () => {
sinon.assert.calledOnce(start)
sinon.assert.calledWithMatch(start, {
command: 'non_existent_module_test.js',
file: 'non_existent_module_test.js',
shell: false,
})
sinon.assert.calledOnce(error)
done()
})
})
})

describe('callArgs on context', () => {
function injectTestEnv (context) {
if (!context.callArgs) return
const args = context.callArgs
const opts = args[2] != null && typeof args[2] === 'object' ? args[2] : {}
args[2] = { ...opts, env: { ...process.env, DD_TEST_VAR: 'injected' } }
}

it('should include callArgs for async methods', (done) => {
const child = childProcess.spawn('echo', ['hello'])

child.once('close', () => {
sinon.assert.calledOnce(start)
const context = start.firstCall.firstArg
assert.ok(Array.isArray(context.callArgs))
assert.strictEqual(context.callArgs[0], 'echo')
assert.deepStrictEqual(context.callArgs[1], ['hello'])
done()
})
})

it('should include callArgs for sync methods', () => {
childProcess.spawnSync('echo', ['hello'])

sinon.assert.calledOnce(start)
const context = start.firstCall.firstArg
assert.ok(Array.isArray(context.callArgs))
assert.strictEqual(context.callArgs[0], 'echo')
assert.deepStrictEqual(context.callArgs[1], ['hello'])
})

it('should allow subscribers to mutate callArgs for async methods', (done) => {
childProcessChannel.subscribe({ start: injectTestEnv })

const script = 'process.exit(process.env.DD_TEST_VAR === "injected" ? 0 : 1)'
const child = childProcess.spawn('node', ['-e', script])

child.once('close', (code) => {
childProcessChannel.unsubscribe({ start: injectTestEnv })
assert.strictEqual(code, 0)
done()
})
})

it('should allow subscribers to mutate callArgs for sync methods', () => {
childProcessChannel.subscribe({ start: injectTestEnv })

const script = 'process.exit(process.env.DD_TEST_VAR === "injected" ? 0 : 1)'
const result = childProcess.spawnSync('node', ['-e', script])

childProcessChannel.unsubscribe({ start: injectTestEnv })
assert.strictEqual(result.status, 0)
})
})
})
})
})
4 changes: 4 additions & 0 deletions packages/dd-trace/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const VALID_PROPAGATION_BEHAVIOR_EXTRACT = new Set(['continue', 'restart', 'igno
const VALID_LOG_LEVELS = new Set(['debug', 'info', 'warn', 'error'])
const DEFAULT_OTLP_PORT = 4318
const RUNTIME_ID = uuid()
// eslint-disable-next-line eslint-rules/eslint-process-env -- internal propagation, not user config
const ROOT_SESSION_ID = process.env.DD_ROOT_JS_SESSION_ID || RUNTIME_ID
const NAMING_VERSIONS = new Set(['v0', 'v1'])
const DEFAULT_NAMING_VERSION = 'v0'

Expand Down Expand Up @@ -145,6 +147,8 @@ class Config {
'runtime-id': RUNTIME_ID,
})

this.rootSessionId = ROOT_SESSION_ID

if (this.isCiVisibility) {
tagger.add(this.tags, {
[ORIGIN_KEY]: 'ciapp-test',
Expand Down
5 changes: 5 additions & 0 deletions packages/dd-trace/src/telemetry/send-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,17 @@ let agentTelemetry = true
* @returns {Record<string, string>}
*/
function getHeaders (config, application, reqType) {
const sessionId = config.tags['runtime-id']
const headers = {
'content-type': 'application/json',
'dd-telemetry-api-version': 'v2',
'dd-telemetry-request-type': reqType,
'dd-client-library-language': application.language_name,
'dd-client-library-version': application.tracer_version,
'dd-session-id': sessionId,
}
if (config.rootSessionId && config.rootSessionId !== sessionId) {
headers['dd-root-session-id'] = config.rootSessionId
}
const debug = config.telemetry && config.telemetry.debug
if (debug) {
Expand Down
78 changes: 78 additions & 0 deletions packages/dd-trace/src/telemetry/session-propagation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use strict'

const dc = require('dc-polyfill')

const childProcessChannel = dc.tracingChannel('datadog:child_process:execution')

let subscribed = false
let rootSessionId
let runtimeId

function injectSessionEnv (existingEnv) {
// eslint-disable-next-line eslint-rules/eslint-process-env -- not in supported-configurations.json
const base = existingEnv == null ? process.env : existingEnv
return {
...base,
DD_ROOT_JS_SESSION_ID: rootSessionId,
DD_PARENT_JS_SESSION_ID: runtimeId,
}
}

function findOptionsIndex (args, shell) {
if (Array.isArray(args[1])) {
return { index: 2, exists: args[2] != null && typeof args[2] === 'object' }
}
if (args[1] != null && typeof args[1] === 'object') {
return { index: 1, exists: true }
}
if (!shell && args[2] != null && typeof args[2] === 'object') {
return { index: 2, exists: true }
}
return { index: shell ? 1 : 2, exists: false }
}

function onChildProcessStart (context) {
if (!context.callArgs) return

const args = context.callArgs
const { index, exists } = findOptionsIndex(args, context.shell)

if (exists) {
args[index] = { ...args[index], env: injectSessionEnv(args[index].env) }
return
}

const opts = { env: injectSessionEnv(null) }

if (!context.shell && !Array.isArray(args[1])) {
args.splice(1, 0, [])
}

if (typeof args[index] === 'function') {
args.splice(index, 0, opts)
} else {
args[index] = opts
}
}

const handler = { start: onChildProcessStart }

function start (config) {
if (!config.telemetry?.enabled || subscribed) return
subscribed = true

rootSessionId = config.rootSessionId
runtimeId = config.tags['runtime-id']
Comment thread
khanayan123 marked this conversation as resolved.

childProcessChannel.subscribe(handler)
}

function stop () {
if (!subscribed) return
childProcessChannel.unsubscribe(handler)
subscribed = false
rootSessionId = undefined
runtimeId = undefined
}

module.exports = { start, stop, _onChildProcessStart: onChildProcessStart }
3 changes: 3 additions & 0 deletions packages/dd-trace/src/telemetry/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const endpoints = require('./endpoints')
const { sendData } = require('./send-data')
const { manager: metricsManager } = require('./metrics')
const telemetryLogger = require('./logs')
const sessionPropagation = require('./session-propagation')

/**
* @typedef {Record<string, unknown>} TelemetryPayloadObject
Expand Down Expand Up @@ -370,6 +371,7 @@ function start (aConfig, thePluginManager) {
dependencies.start(config, application, host, getRetryData, updateRetryData)
telemetryLogger.start(config)
endpoints.start(config, application, host, getRetryData, updateRetryData)
sessionPropagation.start(config)

sendData(config, application, host, 'app-started', appStarted(config))

Expand Down Expand Up @@ -397,6 +399,7 @@ function stop () {
telemetryStopChannel.publish(getTelemetryData())

endpoints.stop()
sessionPropagation.stop()
config = undefined
}

Expand Down
Loading
Loading