Skip to content

Commit

Permalink
Merge branch 'master' into ida613/baggage
Browse files Browse the repository at this point in the history
  • Loading branch information
ida613 authored Oct 22, 2024
2 parents cadc687 + e7edfcf commit 97ccd23
Show file tree
Hide file tree
Showing 18 changed files with 374 additions and 69 deletions.
31 changes: 27 additions & 4 deletions integration-tests/debugger/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,9 @@ describe('Dynamic Instrumentation', function () {
elements: [
{ type: 'number', value: '1' },
{ type: 'number', value: '2' },
{ type: 'number', value: '3' }
{ type: 'number', value: '3' },
{ type: 'number', value: '4' },
{ type: 'number', value: '5' }
]
},
obj: {
Expand Down Expand Up @@ -556,6 +558,27 @@ describe('Dynamic Instrumentation', function () {

agent.addRemoteConfig(generateRemoteConfig({ captureSnapshot: true, capture: { maxLength: 10 } }))
})

it('should respect maxCollectionSize', (done) => {
agent.on('debugger-input', ({ payload: { 'debugger.snapshot': { captures } } }) => {
const { locals } = captures.lines[probeLineNo]

assert.deepEqual(locals.arr, {
type: 'Array',
elements: [
{ type: 'number', value: '1' },
{ type: 'number', value: '2' },
{ type: 'number', value: '3' }
],
notCapturedReason: 'collectionSize',
size: 5
})

done()
})

agent.addRemoteConfig(generateRemoteConfig({ captureSnapshot: true, capture: { maxCollectionSize: 3 } }))
})
})
})

Expand Down Expand Up @@ -612,7 +635,9 @@ function generateRemoteConfig (overrides = {}) {
}
}

function generateProbeConfig (overrides) {
function generateProbeConfig (overrides = {}) {
overrides.capture = { maxReferenceDepth: 3, ...overrides.capture }
overrides.sampling = { snapshotsPerSecond: 5000, ...overrides.sampling }
return {
id: randomUUID(),
version: 0,
Expand All @@ -623,8 +648,6 @@ function generateProbeConfig (overrides) {
template: 'Hello World!',
segments: [{ str: 'Hello World!' }],
captureSnapshot: false,
capture: { maxReferenceDepth: 3 },
sampling: { snapshotsPerSecond: 5000 },
evaluateAt: 'EXIT',
...overrides
}
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/debugger/target-app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function getSomeData () {
lstr: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
sym: Symbol('foo'),
regex: /bar/i,
arr: [1, 2, 3],
arr: [1, 2, 3, 4, 5],
obj: {
foo: {
baz: 42,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"bench:e2e:ci-visibility": "node benchmark/e2e-ci/benchmark-run.js",
"type:doc": "cd docs && yarn && yarn build",
"type:test": "cd docs && yarn && yarn test",
"lint": "node scripts/check_licenses.js && eslint . && yarn audit --groups dependencies",
"lint-fix": "node scripts/check_licenses.js && eslint . --fix && yarn audit --groups dependencies",
"lint": "node scripts/check_licenses.js && eslint . && yarn audit",
"lint-fix": "node scripts/check_licenses.js && eslint . --fix && yarn audit",
"services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services",
"test": "SERVICES=* yarn services && mocha --expose-gc 'packages/dd-trace/test/setup/node.js' 'packages/*/test/**/*.spec.js'",
"test:appsec": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" --exclude \"packages/dd-trace/test/appsec/**/*.plugin.spec.js\" \"packages/dd-trace/test/appsec/**/*.spec.js\"",
Expand Down Expand Up @@ -76,7 +76,7 @@
"node": ">=18"
},
"dependencies": {
"@datadog/native-appsec": "8.1.1",
"@datadog/native-appsec": "8.2.1",
"@datadog/native-iast-rewriter": "2.5.0",
"@datadog/native-iast-taint-tracking": "3.1.0",
"@datadog/native-metrics": "^2.0.0",
Expand Down
9 changes: 9 additions & 0 deletions packages/datadog-instrumentations/src/helpers/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ const disabledInstrumentations = new Set(
DD_TRACE_DISABLED_INSTRUMENTATIONS ? DD_TRACE_DISABLED_INSTRUMENTATIONS.split(',') : []
)

// Check for DD_TRACE_<INTEGRATION>_ENABLED environment variables
for (const [key, value] of Object.entries(process.env)) {
const match = key.match(/^DD_TRACE_(.+)_ENABLED$/)
if (match && (value.toLowerCase() === 'false' || value === '0')) {
const integration = match[1].toLowerCase()
disabledInstrumentations.add(integration)
}
}

const loadChannel = channel('dd-trace:instrumentation:load')

// Globals
Expand Down
15 changes: 8 additions & 7 deletions packages/datadog-plugin-fastify/test/suite.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use strict'
// const suiteTest = require('../../dd-trace/test/plugins/suite')

// suiteTest({
// modName: 'fastify',
// repoUrl: 'fastify/fastify',
// commitish: 'latest',
// testCmd: 'node_modules/.bin/tap -J test/*.test.js test/*/*.test.js --no-coverage --no-check-coverage'
// })
const suiteTest = require('../../dd-trace/test/plugins/suite')

suiteTest({
modName: 'fastify',
repoUrl: 'fastify/fastify',
commitish: 'latest',
testCmd: 'node_modules/.bin/tap -J test/*.test.js test/*/*.test.js --no-coverage --no-check-coverage'
})
11 changes: 9 additions & 2 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ class Config {
this._setValue(defaults, 'reportHostname', false)
this._setValue(defaults, 'runtimeMetrics', false)
this._setValue(defaults, 'sampleRate', undefined)
this._setValue(defaults, 'sampler.rateLimit', undefined)
this._setValue(defaults, 'sampler.rateLimit', 100)
this._setValue(defaults, 'sampler.rules', [])
this._setValue(defaults, 'sampler.spanSamplingRules', [])
this._setValue(defaults, 'scope', undefined)
Expand All @@ -545,6 +545,7 @@ class Config {
this._setValue(defaults, 'telemetry.heartbeatInterval', 60000)
this._setValue(defaults, 'telemetry.logCollection', false)
this._setValue(defaults, 'telemetry.metrics', true)
this._setValue(defaults, 'traceEnabled', true)
this._setValue(defaults, 'traceId128BitGenerationEnabled', true)
this._setValue(defaults, 'traceId128BitLoggingEnabled', false)
this._setValue(defaults, 'tracePropagationExtractFirst', false)
Expand Down Expand Up @@ -633,6 +634,7 @@ class Config {
DD_TRACE_BAGGAGE_MAX_ITEMS,
DD_TRACE_CLIENT_IP_ENABLED,
DD_TRACE_CLIENT_IP_HEADER,
DD_TRACE_ENABLED,
DD_TRACE_EXPERIMENTAL_EXPORTER,
DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED,
DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED,
Expand Down Expand Up @@ -722,6 +724,7 @@ class Config {
this._setBoolean(env, 'dsmEnabled', DD_DATA_STREAMS_ENABLED)
this._setBoolean(env, 'dynamicInstrumentationEnabled', DD_DYNAMIC_INSTRUMENTATION_ENABLED)
this._setString(env, 'env', DD_ENV || tags.env)
this._setBoolean(env, 'traceEnabled', DD_TRACE_ENABLED)
this._setBoolean(env, 'experimental.enableGetRumData', DD_TRACE_EXPERIMENTAL_GET_RUM_DATA_ENABLED)
this._setString(env, 'experimental.exporter', DD_TRACE_EXPERIMENTAL_EXPORTER)
this._setBoolean(env, 'experimental.runtimeId', DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED)
Expand Down Expand Up @@ -1168,7 +1171,11 @@ class Config {
}

if (typeof value === 'string') {
value = value.split(',')
value = value.split(',').map(item => {
// Trim each item and remove whitespace around the colon
const [key, val] = item.split(':').map(part => part.trim())
return val !== undefined ? `${key}:${val}` : key
})
}

if (Array.isArray(value)) {
Expand Down
8 changes: 6 additions & 2 deletions packages/dd-trace/src/debugger/devtools_client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ session.on('Debugger.paused', async ({ params }) => {
const timestamp = Date.now()

let captureSnapshotForProbe = null
let maxReferenceDepth, maxLength
let maxReferenceDepth, maxCollectionSize, maxLength
const probes = params.hitBreakpoints.map((id) => {
const probe = breakpoints.get(id)
if (probe.captureSnapshot) {
captureSnapshotForProbe = probe
maxReferenceDepth = highestOrUndefined(probe.capture.maxReferenceDepth, maxReferenceDepth)
maxCollectionSize = highestOrUndefined(probe.capture.maxCollectionSize, maxCollectionSize)
maxLength = highestOrUndefined(probe.capture.maxLength, maxLength)
}
return probe
Expand All @@ -38,7 +39,10 @@ session.on('Debugger.paused', async ({ params }) => {
if (captureSnapshotForProbe !== null) {
try {
// TODO: Create unique states for each affected probe based on that probes unique `capture` settings (DEBUG-2863)
processLocalState = await getLocalStateForCallFrame(params.callFrames[0], { maxReferenceDepth, maxLength })
processLocalState = await getLocalStateForCallFrame(
params.callFrames[0],
{ maxReferenceDepth, maxCollectionSize, maxLength }
)
} catch (err) {
// TODO: This error is not tied to a specific probe, but to all probes with `captureSnapshot: true`.
// However, in 99,99% of cases, there will be just a single probe, so I guess this simplification is ok?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict'

const { collectionSizeSym } = require('./symbols')
const session = require('../session')

const LEAF_SUBTYPES = new Set(['date', 'regexp'])
Expand All @@ -14,56 +15,67 @@ module.exports = {
// each lookup will just finish in its own time and traverse the child nodes when the event loop allows it.
// Alternatively, use `Promise.all` or something like that, but the code would probably be more complex.

async function getObject (objectId, maxDepth, depth = 0) {
async function getObject (objectId, opts, depth = 0, collection = false) {
const { result, privateProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
})

if (privateProperties) result.push(...privateProperties)
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.splice(opts.maxCollectionSize)
result[collectionSizeSym] = size
}
} else if (privateProperties) {
result.push(...privateProperties)
}

return traverseGetPropertiesResult(result, maxDepth, depth)
return traverseGetPropertiesResult(result, opts, depth)
}

async function traverseGetPropertiesResult (props, maxDepth, 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 >= maxDepth) return props
if (depth >= opts.maxReferenceDepth) return props

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
prop.value.properties = await getObjectProperties(subtype, objectId, maxDepth, depth)
prop.value.properties = await getObjectProperties(subtype, objectId, opts, depth)
} else if (type === 'function') {
prop.value.properties = await getFunctionProperties(objectId, maxDepth, depth + 1)
prop.value.properties = await getFunctionProperties(objectId, opts, depth + 1)
}
}

return props
}

async function getObjectProperties (subtype, objectId, maxDepth, depth) {
async function getObjectProperties (subtype, objectId, opts, depth) {
if (ITERABLE_SUBTYPES.has(subtype)) {
return getIterable(objectId, maxDepth, depth)
return getIterable(objectId, opts, depth)
} else if (subtype === 'promise') {
return getInternalProperties(objectId, maxDepth, depth)
return getInternalProperties(objectId, opts, depth)
} else if (subtype === 'proxy') {
return getProxy(objectId, maxDepth, depth)
return getProxy(objectId, opts, depth)
} else if (subtype === 'arraybuffer') {
return getArrayBuffer(objectId, maxDepth, depth)
return getArrayBuffer(objectId, opts, depth)
} else {
return getObject(objectId, maxDepth, depth + 1)
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, maxDepth, depth) {
async function getFunctionProperties (objectId, opts, depth) {
let { result } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
Expand All @@ -72,10 +84,12 @@ async function getFunctionProperties (objectId, maxDepth, depth) {
// For legacy reasons (I assume) functions has a `prototype` property besides the internal `[[Prototype]]`
result = result.filter(({ name }) => name !== 'prototype')

return traverseGetPropertiesResult(result, maxDepth, depth)
return traverseGetPropertiesResult(result, opts, depth)
}

async function getIterable (objectId, maxDepth, 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
Expand All @@ -93,10 +107,17 @@ async function getIterable (objectId, maxDepth, depth) {
ownProperties: true // exclude inherited properties
})

return traverseGetPropertiesResult(result, maxDepth, depth)
removeNonEnumerableProperties(result) // remove the `length` property
const size = result.length
if (size > opts.maxCollectionSize) {
result.splice(opts.maxCollectionSize)
result[collectionSizeSym] = size
}

return traverseGetPropertiesResult(result, opts, depth)
}

async function getInternalProperties (objectId, maxDepth, depth) {
async function getInternalProperties (objectId, opts, depth) {
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
Expand All @@ -105,10 +126,10 @@ async function getInternalProperties (objectId, maxDepth, depth) {
// We want all internal properties except the prototype
const props = internalProperties.filter(({ name }) => name !== '[[Prototype]]')

return traverseGetPropertiesResult(props, maxDepth, depth)
return traverseGetPropertiesResult(props, opts, depth)
}

async function getProxy (objectId, maxDepth, depth) {
async function getProxy (objectId, opts, depth) {
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
Expand All @@ -127,14 +148,14 @@ async function getProxy (objectId, maxDepth, depth) {
ownProperties: true // exclude inherited properties
})

return traverseGetPropertiesResult(result, maxDepth, depth)
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, maxDepth, depth) {
async function getArrayBuffer (objectId, opts, depth) {
const { internalProperties } = await session.post('Runtime.getProperties', {
objectId,
ownProperties: true // exclude inherited properties
Expand All @@ -149,5 +170,13 @@ async function getArrayBuffer (objectId, maxDepth, depth) {
ownProperties: true // exclude inherited properties
})

return traverseGetPropertiesResult(result, maxDepth, depth)
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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { getRuntimeObject } = require('./collector')
const { processRawState } = require('./processor')

const DEFAULT_MAX_REFERENCE_DEPTH = 3
const DEFAULT_MAX_COLLECTION_SIZE = 100
const DEFAULT_MAX_LENGTH = 255

module.exports = {
Expand All @@ -12,14 +13,18 @@ module.exports = {

async function getLocalStateForCallFrame (
callFrame,
{ maxReferenceDepth = DEFAULT_MAX_REFERENCE_DEPTH, maxLength = DEFAULT_MAX_LENGTH } = {}
{
maxReferenceDepth = DEFAULT_MAX_REFERENCE_DEPTH,
maxCollectionSize = DEFAULT_MAX_COLLECTION_SIZE,
maxLength = DEFAULT_MAX_LENGTH
} = {}
) {
const rawState = []
let processedState = null

for (const scope of callFrame.scopeChain) {
if (scope.type === 'global') continue // The global scope is too noisy
rawState.push(...await getRuntimeObject(scope.object.objectId, maxReferenceDepth))
rawState.push(...await getRuntimeObject(scope.object.objectId, { maxReferenceDepth, maxCollectionSize }))
}

// Deplay calling `processRawState` so the caller gets a chance to resume the main thread before processing `rawState`
Expand Down
Loading

0 comments on commit 97ccd23

Please sign in to comment.