dd-trace
Version:
Datadog APM tracing client for JavaScript
255 lines (212 loc) • 7.09 kB
JavaScript
const {
workerData: {
config,
breakpointSetChannel,
breakpointHitChannel,
breakpointRemoveChannel,
},
} = require('worker_threads')
const { randomUUID } = require('crypto')
// TODO: move debugger/devtools_client/session to common place
const session = require('../../../debugger/devtools_client/session')
// TODO: move debugger/devtools_client/source-maps to common place
const { getGeneratedPosition } = require('../../../debugger/devtools_client/source-maps')
// TODO: move debugger/devtools_client/snapshot to common place
const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot')
const {
DEFAULT_MAX_REFERENCE_DEPTH,
DEFAULT_MAX_COLLECTION_SIZE,
DEFAULT_MAX_FIELD_COUNT,
DEFAULT_MAX_LENGTH,
} = require('../../../debugger/devtools_client/snapshot/constants')
// TODO: move debugger/devtools_client/state to common place
const {
findScriptFromPartialPath,
getStackFromCallFrames,
} = require('../../../debugger/devtools_client/state')
const log = require('../../../log')
let processTags
if (config.propagateProcessTags?.enabled) {
processTags = require('../../../process-tags')
processTags.initialize()
}
let sessionStarted = false
let inFlightBreakpointHits = 0
let isBreakpointHitDrainScheduled = false
const breakpointIdToProbe = new Map()
const probeIdToBreakpointId = new Map()
const breakpointHitDrainRequests = []
const limits = {
maxReferenceDepth: DEFAULT_MAX_REFERENCE_DEPTH,
maxCollectionSize: DEFAULT_MAX_COLLECTION_SIZE,
maxFieldCount: DEFAULT_MAX_FIELD_COUNT,
maxLength: DEFAULT_MAX_LENGTH,
}
/**
* @param {object} value - Snapshot object or sub-object.
* @returns {boolean}
*/
function isCapturedValue (value) {
return typeof value.type === 'string'
}
/**
* Remove empty capture containers before sending snapshots through the Test Optimization logs path.
*
* @param {object} value - Snapshot object or sub-object.
* @returns {void}
*/
function removeEmptyCaptureProperties (value) {
const stack = [value]
while (stack.length > 0) {
const current = stack.pop()
if (!current || typeof current !== 'object') continue
if (Array.isArray(current)) {
for (const item of current) {
if (item && typeof item === 'object') {
stack.push(item)
}
}
continue
}
if (isCapturedValue(current)) {
if (current.elements === null || (Array.isArray(current.elements) && current.elements.length === 0)) {
delete current.elements
}
if (current.entries === null || (Array.isArray(current.entries) && current.entries.length === 0)) {
delete current.entries
}
if (current.fields === null || (
current.fields &&
typeof current.fields === 'object' &&
Object.keys(current.fields).length === 0
)) {
delete current.fields
}
}
for (const child of Object.values(current)) {
if (child && typeof child === 'object') {
stack.push(child)
}
}
}
}
session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint], callFrames } }) => {
const probe = breakpointIdToProbe.get(hitBreakpoint)
if (!probe) {
log.warn('No probe found for breakpoint', hitBreakpoint)
return session.post('Debugger.resume')
}
inFlightBreakpointHits++
try {
const stack = await getStackFromCallFrames(callFrames)
const { processLocalState } = await getLocalStateForCallFrame(callFrames[0], limits)
await session.post('Debugger.resume')
const snapshot = {
id: randomUUID(),
timestamp: Date.now(),
probe: {
id: probe.id,
version: 0,
location: probe.location,
},
captures: {
lines: { [probe.location.lines[0]]: { locals: processLocalState() } },
},
stack,
language: 'javascript',
}
removeEmptyCaptureProperties(snapshot.captures)
if (processTags) {
snapshot[processTags.DYNAMIC_INSTRUMENTATION_FIELD_NAME] = processTags.tagsObject
}
breakpointHitChannel.postMessage({ snapshot })
} finally {
inFlightBreakpointHits--
scheduleBreakpointHitDrain()
}
})
breakpointHitChannel.on('message', ({ drainRequestId }) => {
if (!drainRequestId) return
breakpointHitDrainRequests.push(drainRequestId)
scheduleBreakpointHitDrain()
})
breakpointRemoveChannel.on('message', async (probeId) => {
await removeBreakpoint(probeId)
breakpointRemoveChannel.postMessage(probeId)
})
breakpointSetChannel.on('message', async (probe) => {
await addBreakpoint(probe)
breakpointSetChannel.postMessage(probe.id)
})
async function removeBreakpoint (probeId) {
if (!sessionStarted) {
// We should not get in this state, but abort if we do, so the code doesn't fail unexpected
throw new Error(`Cannot remove probe ${probeId}: Debugger not started`)
}
const breakpointId = probeIdToBreakpointId.get(probeId)
if (!breakpointId) {
throw new Error(`Unknown probe id: ${probeId}`)
}
await session.post('Debugger.removeBreakpoint', { breakpointId })
probeIdToBreakpointId.delete(probeId)
breakpointIdToProbe.delete(breakpointId)
}
async function addBreakpoint (probe) {
if (!sessionStarted) await start()
const { file, line } = probe
probe.location = { file, lines: [String(line)] }
const script = findScriptFromPartialPath(file)
if (!script) {
log.error('No loaded script found for', file)
throw new Error(`No loaded script found for ${file}`)
}
const { url, scriptId, sourceMapURL, source } = script
log.warn('Adding breakpoint at %s:%s', url, line)
let lineNumber = line
let columnNumber = 0
if (sourceMapURL) {
try {
({ line: lineNumber, column: columnNumber } = await getGeneratedPosition(url, source, line, sourceMapURL))
} catch (err) {
log.error('Error processing script with source map', err)
}
if (lineNumber === null) {
log.error('Could not find generated position for %s:%s', url, line)
lineNumber = line
columnNumber = 0
}
}
try {
const { breakpointId } = await session.post('Debugger.setBreakpoint', {
location: {
scriptId,
lineNumber: lineNumber - 1,
columnNumber,
},
})
breakpointIdToProbe.set(breakpointId, probe)
probeIdToBreakpointId.set(probe.id, breakpointId)
} catch (e) {
log.error('Error setting breakpoint at %s:%s', url, line, e)
}
}
function start () {
sessionStarted = true
return session.post('Debugger.enable') // return instead of await to reduce number of promises created
}
function drainBreakpointHitRequests () {
if (inFlightBreakpointHits !== 0) return
for (const drainRequestId of breakpointHitDrainRequests) {
breakpointHitChannel.postMessage({ drainRequestId })
}
breakpointHitDrainRequests.length = 0
}
function scheduleBreakpointHitDrain () {
if (isBreakpointHitDrainScheduled) return
isBreakpointHitDrainScheduled = true
setImmediate(() => {
isBreakpointHitDrainScheduled = false
drainBreakpointHitRequests()
})
}