dd-trace
Version:
Datadog APM tracing client for JavaScript
192 lines (158 loc) • 7.26 kB
JavaScript
const { collectionSizeSym, fieldCountSym } = require('./symbols')
const session = require('../session')
const LEAF_SUBTYPES = new Set(['date', 'regexp'])
const ITERABLE_SUBTYPES = new Set(['map', 'set', 'weakmap', 'weakset'])
module.exports = {
getRuntimeObject: getObject
}
async function getObject (objectId, opts, depth = 0, collection = false) {
const { result, privateProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})
if (collection) {
// Trim the collection if it's too large.
// Collections doesn't contain private properties, so the code in this block doesn't have to deal with it.
removeNonEnumerableProperties(result) // remove the `length` property
const size = result.length
if (size > opts.maxCollectionSize) {
result.length = opts.maxCollectionSize
result[collectionSizeSym] = size
}
} else if (result.length > opts.maxFieldCount) {
// Trim the number of properties on the object if there's too many.
const size = result.length
result.length = opts.maxFieldCount
result[fieldCountSym] = size
} else if (privateProperties) {
result.push(...privateProperties)
}
return traverseGetPropertiesResult(result, opts, depth)
}
async function traverseGetPropertiesResult (props, opts, depth) {
// TODO: Decide if we should filter out non-enumerable properties or not:
// props = props.filter((e) => e.enumerable)
if (depth >= opts.maxReferenceDepth) return props
const promises = []
for (const prop of props) {
if (prop.value === undefined) continue
const { value: { type, objectId, subtype } } = prop
if (type === 'object') {
if (objectId === undefined) continue // if `subtype` is "null"
if (LEAF_SUBTYPES.has(subtype)) continue // don't waste time with these subtypes
promises.push(getObjectProperties(subtype, objectId, opts, depth).then((properties) => {
prop.value.properties = properties
}))
} else if (type === 'function') {
promises.push(getFunctionProperties(objectId, opts, depth + 1).then((properties) => {
prop.value.properties = properties
}))
}
}
if (promises.length) {
await Promise.all(promises)
}
return props
}
function getObjectProperties (subtype, objectId, opts, depth) {
if (ITERABLE_SUBTYPES.has(subtype)) {
return getIterable(objectId, opts, depth)
} else if (subtype === 'promise') {
return getInternalProperties(objectId, opts, depth)
} else if (subtype === 'proxy') {
return getProxy(objectId, opts, depth)
} else if (subtype === 'arraybuffer') {
return getArrayBuffer(objectId, opts, depth)
}
return getObject(objectId, opts, depth + 1, subtype === 'array' || subtype === 'typedarray')
}
// TODO: The following extra information from `internalProperties` might be relevant to include for functions:
// - Bound function: `[[TargetFunction]]`, `[[BoundThis]]` and `[[BoundArgs]]`
// - Non-bound function: `[[FunctionLocation]]`, and `[[Scopes]]`
async function getFunctionProperties (objectId, opts, depth) {
let { result } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})
// For legacy reasons (I assume) functions has a `prototype` property besides the internal `[[Prototype]]`
result = result.filter(({ name }) => name !== 'prototype')
return traverseGetPropertiesResult(result, opts, depth)
}
async function getIterable (objectId, opts, depth) {
// TODO: If the iterable has any properties defined on the object directly, instead of in its collection, they will
// exist in the return value below in the `result` property. We currently do not collect these.
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})
let entry = internalProperties[1]
if (entry.name !== '[[Entries]]') {
// Currently `[[Entries]]` is the last of 2 elements, but in case this ever changes, fall back to searching
entry = internalProperties.findLast(({ name }) => name === '[[Entries]]')
}
// Skip the `[[Entries]]` level and go directly to the content of the iterable
const { result } = await session.post('Runtime.getProperties', {
objectId: entry.value.objectId,
ownProperties: true // exclude inherited properties
})
removeNonEnumerableProperties(result) // remove the `length` property
const size = result.length
if (size > opts.maxCollectionSize) {
result.length = opts.maxCollectionSize
result[collectionSizeSym] = size
}
return traverseGetPropertiesResult(result, opts, depth)
}
async function getInternalProperties (objectId, opts, depth) {
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})
// We want all internal properties except the prototype
const props = internalProperties.filter(({ name }) => name !== '[[Prototype]]')
return traverseGetPropertiesResult(props, opts, depth)
}
async function getProxy (objectId, opts, depth) {
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})
// TODO: If we do not skip the proxy wrapper, we can add a `revoked` boolean
let entry = internalProperties[1]
if (entry.name !== '[[Target]]') {
// Currently `[[Target]]` is the last of 2 elements, but in case this ever changes, fall back to searching
entry = internalProperties.findLast(({ name }) => name === '[[Target]]')
}
// Skip the `[[Target]]` level and go directly to the target of the Proxy
const { result } = await session.post('Runtime.getProperties', {
objectId: entry.value.objectId,
ownProperties: true // exclude inherited properties
})
return traverseGetPropertiesResult(result, opts, depth)
}
// Support for ArrayBuffer is a bit trickly because the internal structure stored in `internalProperties` is not
// documented and is not straight forward. E.g. ArrayBuffer(3) will internally contain both Int8Array(3) and
// UInt8Array(3), whereas ArrayBuffer(8) internally contains both Int8Array(8), Uint8Array(8), Int16Array(4), and
// Int32Array(2) - all representing the same data in different ways.
async function getArrayBuffer (objectId, opts, depth) {
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})
// Use Uint8 to make it easy to convert to a string later.
const entry = internalProperties.find(({ name }) => name === '[[Uint8Array]]')
// Skip the `[[Uint8Array]]` level and go directly to the content of the ArrayBuffer
const { result } = await session.post('Runtime.getProperties', {
objectId: entry.value.objectId,
ownProperties: true // exclude inherited properties
})
return traverseGetPropertiesResult(result, opts, depth)
}
function removeNonEnumerableProperties (props) {
for (let i = 0; i < props.length; i++) {
if (props[i].enumerable === false) {
props.splice(i--, 1)
}
}
}