🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

dd-trace

Package Overview
Dependencies
Maintainers
1
Versions
710
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

dd-trace - npm Package Compare versions

Comparing version
5.112.0
to
5.113.0
+31
packages/datadog-i.../rewriter/instrumentations/mercurius.js
'use strict'
// Mercurius funnels every GraphQL request through the named async function
// `fastifyGraphQl` (decorated as both `app.graphql` and, via `reply.graphql`,
// the per-request entry point). Wrapping that one function gives a single
// top-level span per operation regardless of how the query reaches mercurius
// — HTTP POST/GET, batched queries, or a programmatic `app.graphql()` call —
// and regardless of whether the query runs through graphql's `execute` (cold)
// or a JIT-compiled query (warm), where no `graphql.execute` span fires.
//
// The function name and signature `(source, context, variables, operationName)`
// are stable across the supported major range (verified against 10.x and 16.x),
// so a `functionName` match needs no per-version file paths.
module.exports = [
{
module: {
// Floor at 13: it is the oldest major whose fastify-plugin peer (^4)
// accepts fastify 4, which installs and runs on the oldest supported Node
// (18). 15+ requires Node 20 and fastify 5, covered on the latest-Node CI
// leg. The `fastifyGraphQl` funnel is unchanged across this whole range.
name: 'mercurius',
versionRange: '>=13',
filePath: 'index.js',
},
functionQuery: {
functionName: 'fastifyGraphQl',
kind: 'Async',
},
channelName: 'apm:graphql:request',
},
]
// AUTO-GENERATED by dd-compile
// Orchestrion rewriter handles the actual instrumentation via JSON config.
// This file registers the module hooks for the rewriter to process.
'use strict'
const { addHook, getHooks } = require('./helpers/instrument')
for (const hook of getHooks('mercurius')) {
addHook(hook, exports => exports)
}
'use strict'
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
const { extractErrorIntoSpanEvent, getCachedRequestOperation } = require('./utils')
// Top-level GraphQL request span for drivers that funnel every operation
// through a single entry point but parse/validate/execute internally (mercurius
// today). It parents the `graphql.parse`/`graphql.validate`/`graphql.execute`
// (or JIT) sub-spans and carries the request text + operation name/type, which
// otherwise have no home when the query is JIT-compiled and `graphql.execute`
// never fires.
//
// The entry boundary only hands us the raw `source` (string or pre-parsed AST)
// and `operationName`; the parsed document — and therefore the precise
// operation signature — is only known once mercurius parses internally. On the
// cold path the `validate` sub-plugin refines the resource/operation tags onto
// this span via `ctx.currentStore.graphqlRequestSpan` once the document is
// available, so we never re-parse on the hot path. On the JIT warm path no
// sub-span fires, so we recover the same tags from the cache the cold path
// populated, keyed by source + operationName.
class GraphQLRequestPlugin extends TracingPlugin {
static id = 'graphql'
static operation = 'request'
static type = 'graphql'
static kind = 'server'
static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request'
bindStart (ctx) {
// fastifyGraphQl(source, context, variables, operationName)
const source = ctx.arguments?.[0]
const operationName = ctx.arguments?.[3]
// `source` is the request text on the common path, but mercurius also
// accepts a pre-parsed document AST; only a string is the query text, and
// `graphql.source` carries only the text form.
const docSource = typeof source === 'string' ? source : undefined
// Warm (JIT-compiled) path: execute never fires, so recover the operation
// signature/type the cold path cached, keyed by source + operationName —
// by query text for a string, by document identity for a pre-parsed AST.
// Empty on the cold path — validate hasn't refined yet — where the request
// span is refined from the parsed document instead.
const cached = getCachedRequestOperation(source, operationName)
const span = this.startSpan(this.operationName({ id: 'request' }), {
service: this.config.service || this.serviceName(),
// The cached signature is the precise resource; otherwise provisional and
// refined by the validate sub-plugin once the document is parsed.
// `operationName` is the best name at the boundary; falls back to the
// operation signature once validate sees the document.
resource: cached?.signature || operationName || undefined,
kind: this.constructor.kind,
type: this.constructor.type,
meta: {
'graphql.operation.type': cached?.type,
'graphql.operation.name': cached?.name || operationName,
'graphql.source': this.config.source ? docSource : undefined,
},
}, ctx)
// Hand the span, the requested operation name, and the raw source to the
// validate sub-plugin running inside this store so it can refine the
// resource + operation tags from the parsed document (validate is the first
// boundary that has it) and cache them keyed by the source the request
// boundary saw. The raw source is the cache key — validate sees mercurius's
// internally parsed document, not the caller's source, and for a pre-parsed
// AST the two are different objects.
ctx.currentStore.graphqlRequestSpan = span
ctx.currentStore.graphqlRequestOperationName = operationName
ctx.currentStore.graphqlRequestSource = source
return ctx.currentStore
}
asyncEnd (ctx) {
/* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */
const span = ctx?.currentStore?.span || this.activeSpan
/* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */
if (!span) return ctx.parentStore
const result = ctx.result
if (result?.errors?.length) {
span.setTag('error', result.errors[0])
for (const error of result.errors) {
extractErrorIntoSpanEvent(this._tracerConfig, span, error)
}
}
span.finish()
return ctx.parentStore
}
error (ctx) {
/* istanbul ignore next: currentStore is populated for request errors; activeSpan is base-plugin fallback. */
const span = ctx?.currentStore?.span || this.activeSpan
/* istanbul ignore else: errors are only routed after the request span has started. */
if (span && ctx?.error) {
span.setTag('error', ctx.error)
}
}
}
module.exports = GraphQLRequestPlugin
+4
-2

@@ -78,4 +78,6 @@ /**

const require = Module.createRequire(import.meta.url)
require('./init.js')
if (Module.register) {
const initialized = require('./init.js')
// Only register the loader hook when instrumentation initialized. On a bailout the
// loader has nothing to instrument and can keep a short-lived process from exiting.
if (Module.register && initialized) {
// The loader builds its own include/exclude matcher in `initialize`, so no

@@ -82,0 +84,0 @@ // options need to cross the registration boundary.

{
"name": "dd-trace",
"version": "5.112.0",
"version": "5.113.0",
"description": "Datadog APM tracing client for JavaScript",

@@ -194,3 +194,3 @@ "main": "index.js",

"@types/node": "^18.19.106",
"@types/sinon": "^21.0.1",
"@types/sinon": "^22.0.0",
"axios": "^1.18.1",

@@ -234,2 +234,3 @@ "benchmark": "^2.1.4",

"sinon": "^22.0.0",
"source-map-support": "^0.5.21",
"test-exclude": "^8.0.0",

@@ -236,0 +237,0 @@ "tiktoken": "^1.0.21",

@@ -51,2 +51,3 @@ 'use strict'

'@opensearch-project/opensearch': () => require('../opensearch'),
'@opentelemetry/sdk-trace': () => require('../otel-sdk-trace'),
'@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'),

@@ -109,2 +110,3 @@ '@prisma/client': { esmFirst: true, fn: () => require('../prisma') },

memcached: () => require('../memcached'),
mercurius: () => require('../mercurius'),
'microgateway-core': () => require('../microgateway-core'),

@@ -111,0 +113,0 @@ mocha: () => require('../mocha'),

@@ -40,2 +40,6 @@ 'use strict'

// Keep the marker split: source-map scanners can read a contiguous token in
// string literals as this file's own inline map.
const SOURCE_MAP_PREFIX = '//# sourceMapping' + 'URL=data:application/json;base64,'
function rewrite (content, filename, format) {

@@ -70,3 +74,3 @@ if (!content) return content

return code + '\n' + `//# sourceMappingURL=data:application/json;base64,${inlineMap}`
return code + '\n' + SOURCE_MAP_PREFIX + inlineMap
} catch (e) {

@@ -73,0 +77,0 @@ log.error(e)

@@ -11,2 +11,3 @@ 'use strict'

...require('./langgraph'),
...require('./mercurius'),
...require('./modelcontextprotocol-sdk'),

@@ -13,0 +14,0 @@ ...require('./playwright'),

@@ -11,2 +11,6 @@ 'use strict'

const assert = require('node:assert')
const clone = require('../../../../../vendor/dist/rfdc')({ proto: false, circles: false })
const { parse, query } = require('./compiler')

@@ -32,7 +36,10 @@

const returnIndex = statements.findIndex(statement => (
statement.type === 'ReturnStatement' && statement.argument?.name === 'result'
))
const returnIndex = statements.findIndex(statement =>
statement.type === 'ReturnStatement' && statement.argument
)
if (returnIndex === -1) return
// The generated fulfillment handler always ends in a return; a miss means the
// upstream template changed and the caller's try/catch falls back to the
// unwrapped source.
assert(returnIndex !== -1, 'waitForAsyncEnd: no return statement to wait on')

@@ -43,3 +50,3 @@ const waitStatements = parse(`

if (__apm$asyncEndPromise && typeof __apm$asyncEndPromise.then === 'function') {
return __apm$asyncEndPromise.then(() => result, () => result);
return __apm$asyncEndPromise.then(() => __apm$result, () => __apm$result);
}

@@ -49,3 +56,10 @@ }

// Resolve to whatever the fulfillment handler returns (its return argument),
// so a subscriber that reassigned `__apm$ctx.result` in `asyncEnd` still wins.
const returnArgument = statements[returnIndex].argument
const { arguments: onSettled } = waitStatements[1].consequent.body[0].argument
onSettled[0].body = clone(returnArgument)
onSettled[1].body = clone(returnArgument)
statements.splice(returnIndex, 0, ...waitStatements)
}

@@ -19,2 +19,17 @@ 'use strict'

})
// As of @opentelemetry/sdk-node 0.220.0, NodeSDK builds its provider from
// @opentelemetry/sdk-trace's TracerProvider instead of sdk-trace-node's
// NodeTracerProvider, so the hook above no longer intercepts it. Wrap this
// export too, otherwise DD_TRACE_OTEL_ENABLED spans never reach the tracer.
addHook({
name: '@opentelemetry/sdk-trace',
file: 'build/src/TracerProvider.js',
versions: ['*'],
}, (mod) => {
shimmer.wrap(mod, 'TracerProvider', () => {
return tracer.TracerProvider
})
return mod
})
}

@@ -21,0 +36,0 @@

@@ -460,3 +460,2 @@ 'use strict'

codeOwnersEntries,
testEnvironmentMetadata,
} = testSessionConfiguration

@@ -471,3 +470,2 @@ repositoryRoot = receivedRepositoryRoot || repositoryRoot

_ddCodeOwnersEntries: codeOwnersEntries,
_ddTestEnvironmentMetadata: testEnvironmentMetadata,
}, 'Could not send test session configuration to workers.')

@@ -474,0 +472,0 @@ }

@@ -119,3 +119,2 @@ 'use strict'

_ddCodeOwnersEntries: codeOwnersEntries,
_ddTestEnvironmentMetadata: testEnvironmentMetadata,
} = globalThis.__vitest_worker__.providedContext

@@ -142,3 +141,2 @@

codeOwnersEntries,
testEnvironmentMetadata,
}

@@ -166,3 +164,2 @@ } catch {

codeOwnersEntries: undefined,
testEnvironmentMetadata: undefined,
}

@@ -169,0 +166,0 @@ }

@@ -660,3 +660,2 @@ 'use strict'

codeOwnersEntries: providedContext.codeOwnersEntries,
testEnvironmentMetadata: providedContext.testEnvironmentMetadata,
}

@@ -663,0 +662,0 @@ testSuiteStartCh.runStores(testSuiteCtx, () => {})

@@ -8,8 +8,6 @@ 'use strict'

const GraphQLParsePlugin = require('./parse')
const { extractErrorIntoSpanEvent, getSignature } = require('./utils')
const { extractErrorIntoSpanEvent, getOperation, getSignature } = require('./utils')
const legacyStorage = storage('legacy')
const types = new Set(['query', 'mutation', 'subscription'])
const iastResolveCh = dc.channel('apm:graphql:resolve:start')

@@ -119,5 +117,7 @@ const resolverStartCh = dc.channel('datadog:graphql:resolver:start')

const signature = getSignature(document, name, type, this.config.signature)
const span = this.startSpan(this.operationName(), {
service: this.config.service || this.serviceName(),
resource: getSignature(document, name, type, this.config.signature),
resource: signature,
kind: this.constructor.kind,

@@ -674,13 +674,2 @@ type: this.constructor.type,

function getOperation (document, operationName) {
if (!document || !Array.isArray(document.definitions)) return
for (const definition of document.definitions) {
if (definition && types.has(definition.operation) &&
(!operationName || definition.name?.value === operationName)) {
return definition
}
}
}
function addVariableTags (config, span, variableValues) {

@@ -687,0 +676,0 @@ if (!variableValues || !config.variables) return

@@ -9,2 +9,3 @@ 'use strict'

const GraphQLParsePlugin = require('./parse')
const GraphQLRequestPlugin = require('./request')
const GraphQLValidatePlugin = require('./validate')

@@ -18,2 +19,7 @@

parse: GraphQLParsePlugin,
// Top-level request span for drivers (mercurius) that funnel through a
// single entry point and parse/execute internally. graphql-js, apollo,
// and yoga produce no such channel, so the plugin simply never fires for
// them.
request: GraphQLRequestPlugin,
validate: GraphQLValidatePlugin,

@@ -20,0 +26,0 @@ // resolve plugin is absorbed into execute: per-field data is recorded

'use strict'
const { LRUCache } = require('../../../vendor/dist/lru-cache')
/**
* @typedef {{ signature?: string, type?: string, name?: string }} RequestOperation
*/
const operationTypes = new Set(['query', 'mutation', 'subscription'])
// Mercurius funnels every operation through `fastifyGraphQl`, but the parsed
// document — and therefore the operation signature/type/name — is only known
// once mercurius parses internally. The top-level request span opens before
// that, and on the JIT warm path neither parse/validate nor execute fires, so
// the span would otherwise be left with only the provisional resource. The cold
// path caches the computed metadata; the request boundary reads it back on the
// warm path. Bounded so a flood of distinct queries can't grow it without limit.
//
// The key is the operation name plus the raw query text, not the source alone:
// mercurius keys its document LRU by source but compiles the JIT for a single
// `operationName`, and the compiled query then serves that operation for every
// later request that shares the source — regardless of the `operationName` those
// requests ask for. A source-only key would hand a warm request for operation B
// the metadata of whichever operation was cached last for that source (A),
// mislabeling the span. Operation names cannot contain a newline, so it is a
// safe separator that keeps the two parts from colliding.
const requestOperationCache = new LRUCache({ max: 500 })
// Mercurius also accepts a pre-parsed document AST as the source, which reaches
// the request boundary as an object rather than query text — so there is no
// string to key the LRU by. Mercurius keys its own document LRU by that source
// object's identity, and the same object reaches the boundary on the warm path,
// so a WeakMap keyed by the caller-owned document recovers the metadata without
// mutating the document and releases with it. The value carries the requested
// operation name so a JIT-only sibling selection is not handed another
// operation's metadata (same reason the string cache keys by operation name).
/** @type {WeakMap<object, Map<string | undefined, RequestOperation>>} */
const documentOperationCache = new WeakMap()
/**
* @param {string} source - The raw query text; the same key mercurius uses.
* @param {string | undefined} operationName - The requested operation name.
* @returns {string}
*/
function requestOperationKey (source, operationName) {
return `${operationName ?? ''}\n${source}`
}
/**
* @param {unknown} source - Query text on the common path; a pre-parsed
* document AST otherwise. Any other shape (mercurius rejects it before
* execute) has no cache entry and yields undefined.
* @param {string | undefined} operationName - The requested operation name.
* @returns {RequestOperation | undefined}
*/
function getCachedRequestOperation (source, operationName) {
if (typeof source === 'string') {
return requestOperationCache.get(requestOperationKey(source, operationName))
}
if (source === null || typeof source !== 'object') return
return documentOperationCache.get(source)?.get(operationName)
}
/**
* A string source keys the text LRU; a document AST keys the WeakMap. Any other
* shape has no usable key — mercurius rejects it before execute, so the warm
* path never reaches the request span for it either.
*
* @param {unknown} source
* @returns {source is string | object}
*/
function isCacheableSource (source) {
return typeof source === 'string' || (source !== null && typeof source === 'object')
}
/**
* Select the operation definition matching `operationName`, or the first one
* when no name is given (graphql/mercurius default selection).
*
* @param {import('graphql').DocumentNode | undefined} document
* @param {string | undefined} operationName
* @returns {import('graphql').OperationDefinitionNode | undefined}
*/
function getOperation (document, operationName) {
/* istanbul ignore if: validate/execute only call this with a parsed GraphQL document. */
if (!document || !Array.isArray(document.definitions)) return
for (const definition of document.definitions) {
if (operationTypes.has(definition?.operation) &&
(!operationName || definition.name?.value === operationName)) {
return definition
}
}
}
/**
* Refine the top-level graphql.request span (mercurius) from the parsed
* document and cache the metadata so the JIT warm path — where no sub-span
* fires — can recover the same tags at the request boundary.
*
* This runs at the first boundary that has the document (validate on the cold
* path, which also precedes a pre-execute validation failure). It is idempotent
* across the later execute boundary via the `ddRequestRefined` flag, and a
* no-op for graphql-js/apollo/yoga, which never open a request span.
*
* Every named operation in the document is cached, not just the selected one:
* a multi-operation document parses once, and a later request may select a
* sibling operation that mercurius then serves exclusively through its JIT path
* (no execute span), so its metadata has to be ready before that happens.
*
* @param {import('../../dd-trace/src/opentracing/span') | undefined} requestSpan
* @param {import('graphql').DocumentNode | undefined} document
* @param {unknown} requestSource - The raw source the request boundary saw:
* query text on the common path, a pre-parsed document AST otherwise. The
* cache is keyed by it, not by the parsed document, so the request boundary
* recovers the metadata on the warm path from the same value mercurius keys
* its own document LRU by. Any other shape has no usable key and is not
* cached (the warm path never reaches this span for it either).
* @param {string | undefined} operationName - The requested operation name.
* @param {boolean} calculateSignature - The graphql plugin's `signature` config.
*/
function refineRequestSpan (requestSpan, document, requestSource, operationName, calculateSignature) {
/* istanbul ignore if: validate only refines after the request span and parsed document exist. */
if (!requestSpan || requestSpan.ddRequestRefined || !document) return
requestSpan.ddRequestRefined = true
const operation = getOperation(document, operationName)
const type = operation?.operation
const name = operation?.name?.value
const signature = getSignature(document, name, type, calculateSignature)
if (signature) requestSpan.setTag('resource.name', signature)
if (type) requestSpan.setTag('graphql.operation.type', type)
if (name) requestSpan.setTag('graphql.operation.name', name)
if (!isCacheableSource(requestSource)) return
// Cache the selected operation under the requested name (undefined selects
// the document's first operation, so it shares the entry with that name).
cacheRequestOperation(requestSource, operationName, { signature, type, name })
// Cache every named operation so a JIT-only sibling selection is labeled from
// this single parse instead of falling back to a bare operation name.
for (const definition of document.definitions) {
const definitionName = definition?.name?.value
if (definitionName === undefined || !operationTypes.has(definition.operation)) continue
if (definitionName === operationName) continue
cacheRequestOperation(requestSource, definitionName, {
signature: getSignature(document, definitionName, definition.operation, calculateSignature),
type: definition.operation,
name: definitionName,
})
}
}
/**
* @param {string | import('graphql').DocumentNode} source - Query text keys the
* text LRU; a caller-owned document AST keys the WeakMap (never mutated).
* @param {string | undefined} operationName - The requested operation name.
* @param {RequestOperation} operation
*/
function cacheRequestOperation (source, operationName, operation) {
if (typeof source === 'string') {
requestOperationCache.set(requestOperationKey(source, operationName), operation)
return
}
let operations = documentOperationCache.get(source)
if (operations === undefined) {
operations = new Map()
documentOperationCache.set(source, operations)
}
operations.set(operationName, operation)
}
/**
* @param {{ errorExtensions?: string[] }} config Resolved plugin config; `errorExtensions` lists the

@@ -87,3 +261,6 @@ * GraphQL error `extensions` keys to copy onto the span event.

extractErrorIntoSpanEvent,
getCachedRequestOperation,
getOperation,
getSignature,
refineRequestSpan,
}
'use strict'
const { storage } = require('../../datadog-core')
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
const GraphQLParsePlugin = require('./parse')
const { extractErrorIntoSpanEvent } = require('./utils')
const { extractErrorIntoSpanEvent, refineRequestSpan } = require('./utils')
const legacyStorage = storage('legacy')
class GraphQLValidatePlugin extends TracingPlugin {

@@ -18,2 +21,22 @@ static id = 'graphql'

// Refine the top-level graphql.request span (mercurius) from the parsed
// document. validate is the first boundary that has it and precedes both
// execute and any pre-execute rejection (unknown field, GET mutation), so a
// failing request still ends up with a resource and operation tags. The
// request span, its operation name, and the raw source ride the active
// store the request boundary entered (validate's own `ctx.currentStore` is
// not populated yet). The cache is keyed by that raw source, not the parsed
// document — for a pre-parsed AST mercurius validates a structuredClone, so
// the document here is a different object from the one the boundary saw and
// recovers on the warm path. No-op for graphql-js/apollo/yoga, which never
// open a request span.
const requestStore = legacyStorage.getStore()
refineRequestSpan(
requestStore?.graphqlRequestSpan,
document,
requestStore?.graphqlRequestSource,
requestStore?.graphqlRequestOperationName,
this.config.signature
)
this.startSpan('graphql.validate', {

@@ -20,0 +43,0 @@ service: this.config.service,

@@ -70,3 +70,2 @@ 'use strict'

codeOwnersEntries: this.codeOwnersEntries,
testEnvironmentMetadata: this.testEnvironmentMetadata,
})

@@ -328,3 +327,2 @@ })

repositoryRoot,
testEnvironmentMetadata,
requestErrorTags,

@@ -339,8 +337,2 @@ testSuiteAbsolutePath,

const { testSessionId, testModuleId } = ctx
if (testEnvironmentMetadata) {
this.testEnvironmentMetadata = {
...this.testEnvironmentMetadata,
...testEnvironmentMetadata,
}
}
this._setRepositoryRoot(repositoryRoot, codeOwnersEntries)

@@ -347,0 +339,0 @@ this.command = testCommand

@@ -10,5 +10,3 @@ 'use strict'

// Tracks spans for which start-invocation has been processed, so that
// end-invocation can gate correctly
const activeInvocations = new WeakSet()
const activeInvocations = new WeakMap()

@@ -35,3 +33,4 @@ /**

activeInvocations.add(span)
const req = { headers: headers ?? {} }
activeInvocations.set(span, req)

@@ -79,3 +78,3 @@ span.addTags({

waf.run({ persistent }, span, undefined, span)
waf.run({ persistent }, req, undefined, span)
} catch (err) {

@@ -106,2 +105,3 @@ log.error('[ASM] Error in Lambda start-invocation handler', err)

const req = activeInvocations.get(span)
activeInvocations.delete(span)

@@ -125,8 +125,8 @@

if (hasPersistentData) {
waf.run({ persistent }, span, undefined, span)
waf.run({ persistent }, req, undefined, span)
}
waf.disposeContext(span)
waf.disposeContext(req)
Reporter.finishRequest(span, null, {}, undefined, span)
Reporter.finishRequest(req, null, {}, undefined, span)
} catch (err) {

@@ -133,0 +133,0 @@ log.error('[ASM] Error in Lambda end-invocation handler', err)

@@ -7,3 +7,3 @@ 'use strict'

const request = require('../exporters/common/request')
const { encode: encodeMsgpack } = require('../msgpack')
const { encode: encodeMsgpack, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack')

@@ -40,4 +40,16 @@ function makeRequest (data, url, cb) {

}
const encodedPayload = encodeMsgpack(payload)
let encodedPayload
try {
encodedPayload = encodeMsgpack(payload)
} catch (error) {
if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
// The msgpack-encoded pipeline-stats payload exceeded the agent
// intake cap. Dropping it locally is safer than letting the
// RangeError crash the host process; the agent would reject the
// oversized payload at the network boundary anyway.
log.error('DataStreamsWriter dropped a payload that exceeded the %d byte chunk cap', MAX_CHUNK_SIZE)
return
}
zlib.gzip(encodedPayload, { level: 1 }, (err, compressedData) => {

@@ -44,0 +56,0 @@ if (err) {

'use strict'
const getConfig = require('../config')
const { MsgpackChunk } = require('../msgpack')
const { MsgpackChunk, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack')
const log = require('../log')

@@ -268,3 +268,24 @@ const { normalizeSpan, eventTimeNano } = require('./tags-processors')

this._encode(bytes, trace)
try {
this._encode(bytes, trace)
} catch (error) {
if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
// The trace, or the queued payload it joined, hit the chunk cap.
// Rolling back just the in-flight trace is unsafe: the string cache
// may already hold subarrays / indices pointing at bytes we'd
// discard, and the next encode would emit stale bytes against a
// smaller string table. Drop the whole queued payload so the next
// encode starts from a clean state. Emitting a partial buffer would
// corrupt the msgpack wire. The virtual `reset()` is called (not
// `_reset()`) so subclasses can clear their own per-payload state
// (e.g. `AgentlessCiVisibilityEncoder._eventCount`).
const dropped = this._traceCount
this.reset()
log.error(
'Trace encoder reset after exceeding the %d byte chunk cap; dropped %d trace(s)',
MAX_CHUNK_SIZE,
dropped
)
return
}

@@ -271,0 +292,0 @@ if (this.#debugEncoding) {

'use strict'
const { MAX_SIZE, OverflowError } = require('../msgpack')
const { normalizeSpan } = require('./tags-processors')

@@ -33,4 +34,14 @@ const { AgentEncoder: BaseEncoder, stringifySpanEvents } = require('./0.4')

const traceSize = this._traceBytes.length + 5
const buffer = Buffer.allocUnsafe(prefixSize + stringSize + traceSize)
const payloadSize = prefixSize + stringSize + traceSize
// The string table and the trace bytes are capped independently, so both
// can sit just under the cap while their concatenation crosses it. `encode`
// never sees this overflow — it only exists once the two are summed here —
// so the writer's flush-time catch drops the payload.
if (payloadSize > MAX_SIZE) {
throw new OverflowError(payloadSize)
}
const buffer = Buffer.allocUnsafe(payloadSize)
buffer[0] = ARRAY_OF_TWO

@@ -37,0 +48,0 @@

@@ -10,3 +10,3 @@ 'use strict'

} = require('../ci-visibility/telemetry')
const { MsgpackChunk } = require('../msgpack')
const { MsgpackChunk, MAX_SIZE, OverflowError } = require('../msgpack')
const { AgentEncoder } = require('./0.4')

@@ -375,2 +375,12 @@ const {

const totalSize = prefixBytes.length + eventsBytes.length
// The metadata prefix (built here, not during `encode`) and the events are
// capped independently, so both can stay under the cap while the assembled
// payload crosses it. An oversized metadata tag also overflows the prefix
// chunk itself inside `_encodePayloadStart` above; either way the tagged
// error propagates to the writer's flush-time catch to drop the payload.
if (totalSize > MAX_SIZE) {
throw new OverflowError(totalSize)
}
const buffer = Buffer.allocUnsafe(totalSize)

@@ -377,0 +387,0 @@ prefixBytes.buffer.copy(buffer, 0, 0, prefixBytes.length)

'use strict'
const { MsgpackChunk } = require('../msgpack')
const { MsgpackChunk, MAX_SIZE: MAX_CHUNK_SIZE } = require('../msgpack')

@@ -9,2 +9,3 @@ const {

} = require('../ci-visibility/telemetry')
const log = require('../log')
const FormData = require('../exporters/common/form-data')

@@ -44,3 +45,20 @@ const { AgentEncoder } = require('./0.4')

this._coveragesCount++
this.encodeCodeCoverage(this._coverageBytes, coverage)
try {
this.encodeCodeCoverage(this._coverageBytes, coverage)
} catch (error) {
if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
// The coverage payload hit the chunk cap. `_coverageBytes` may
// hold a partial entry and a stale `coverages` array prefix. Drop
// the whole queued payload so the next encode starts from a clean
// state; the parent `AgentEncoder` does the equivalent for its own
// bytes / string cache via the inherited `_reset()`.
const dropped = this._coveragesCount
this.reset()
log.error(
'Coverage encoder reset after exceeding the %d byte chunk cap; dropped %d coverage(s)',
MAX_CHUNK_SIZE,
dropped
)
return
}

@@ -47,0 +65,0 @@ distributionMetric(

@@ -6,2 +6,3 @@ 'use strict'

const log = require('../../log')
const { MAX_SIZE: MAX_CHUNK_SIZE } = require('../../msgpack')
const request = require('./request')

@@ -31,3 +32,18 @@ const { safeJSONStringify } = require('./util')

}
const payload = this._encoder.makePayload()
let payload
try {
payload = this._encoder.makePayload()
} catch (error) {
if (error.code !== 'ERR_MSGPACK_CHUNK_OVERFLOW') throw error
// Multi-chunk encoders (v0.5, CI Visibility) only learn the assembled
// payload exceeds the cap when `makePayload` stitches the chunks
// together, after `encode` already returned — so the encode-time catch
// never sees it. Drop the queued payload here instead of letting the
// RangeError escape into the host application; the agent would reject
// the oversized payload at the network boundary anyway.
this._encoder.reset()
log.error('Writer dropped %d trace(s) that exceeded the %d byte chunk cap', count, MAX_CHUNK_SIZE)
done()
return
}
this._sendPayload(payload, count, done)

@@ -34,0 +50,0 @@ } else {

@@ -51,2 +51,4 @@ 'use strict'

var runtimeInfo = 'Incompatible runtime Node.js ' + version + ', supported runtimes: Node.js ' + supportedRange
// When not forced, the process bails out here and may call process.exit() right away;
// forward synchronously so the telemetry child can't outlive us and wedge the exit.
telemetry([

@@ -59,3 +61,3 @@ { name: 'abort', tags: ['reason:incompatible_runtime'] },

result_reason: runtimeInfo
})
}, !forced)
log.info('Aborting application instrumentation due to incompatible_runtime.')

@@ -62,0 +64,0 @@ log.info('Found incompatible runtime Node.js %s, Supported runtimes: Node.js %s.', version, supportedRange)

'use strict'
var fs = require('fs')
var spawn = require('child_process').spawn
// Capture the child_process functions at load time, before the tracer wraps the module.
// Reaching through require('child_process') at send time would route the forwarder through
// the tracer's own child_process instrumentation once the tracer is initialized.
var childProcess = require('child_process')
var spawn = childProcess.spawn
// eslint-disable-next-line n/no-unsupported-features/node-builtins
var spawnSync = childProcess.spawnSync
var tracerVersion = require('../../../../package.json').version

@@ -50,3 +56,3 @@ var log = require('./log')

function sendTelemetry (name, tags, resultMetadata) {
function sendTelemetry (name, tags, resultMetadata, synchronous) {
var points = name

@@ -78,2 +84,33 @@ if (typeof name === 'string') {

var payload = JSON.stringify({ metadata: currentMetadata, points: points })
// A forwarder spawned asynchronously can still be tearing down its stdio pipes when the
// injected app calls process.exit(); on Node 24.0.0/24.1.x that deadlocks the exit
// (fixed upstream in 24.2), hanging short-lived single-step-install processes. On the
// bailout path the caller passes synchronous=true, so the child is fully reaped before we
// return and nothing survives to race the exit. spawnSync is only reached on that path,
// before any instrumentation is active, so it never traces the forwarder. It exists since
// Node 0.11.12; the guardrails still target >=0.8, which predates the exit bug anyway.
if (synchronous && spawnSync) {
// Bound the blocking send: this telemetry is best-effort and the whole point of the
// synchronous path is to avoid a hung exit, so a forwarder that wedges must not become a
// new hard hang. On timeout spawnSync kills the child and returns error.code ETIMEDOUT.
var result = spawnSync(telemetryForwarderPath, ['library_entrypoint'], {
input: payload,
stdio: ['pipe', 'ignore', 'ignore'],
timeout: 1000,
killSignal: 'SIGKILL'
})
if (result.error) {
if (result.error.code === 'ETIMEDOUT') {
log.error('Telemetry forwarder timed out')
} else {
log.error('Failed to spawn telemetry forwarder')
}
} else if (result.status) {
log.error('Telemetry forwarder exited with code', result.status)
}
return
}
var proc = spawn(telemetryForwarderPath, ['library_entrypoint'], {

@@ -93,3 +130,3 @@ stdio: 'pipe'

})
proc.stdin.end(JSON.stringify({ metadata: currentMetadata, points: points }))
proc.stdin.end(payload)
}

@@ -13,4 +13,31 @@ 'use strict'

const SHRINK_USAGE_RATIO = 4
// Hard cap on chunk growth. The agent's trace intake rejects payloads over
// 50 MiB, so anything past that is dead on arrival anyway. A pathological
// single trace (unsanitized meta tag the size of a media file, multi-MB
// stack trace) used to grow the buffer without limit until either the
// allocation failed or the process tripped its memory ceiling. `reserve`
// now refuses the growth with a tagged `RangeError`; `AgentEncoder` catches
// it, resets the in-flight payload, and logs.
const MAX_SIZE = 50 * 1024 * 1024 // 50 MiB
/**
* Thrown when a chunk — or an assembled payload stitched from several chunks —
* would cross `MAX_SIZE`. Shared so the `reserve` cap and the per-encoder
* assembled-size guards (`0.5`, agentless CI Visibility) raise the same `code`,
* which every writer's `flush` recognises to drop the payload instead of
* crashing the host.
*/
class OverflowError extends RangeError {
code = 'ERR_MSGPACK_CHUNK_OVERFLOW'
/**
* @param {number} needed Requested total byte size.
*/
constructor (needed) {
super(`MsgpackChunk capped at ${MAX_SIZE} bytes; requested ${needed}`)
this.name = 'OverflowError'
}
}
/**
* Resizable msgpack write buffer. Owns the byte-layout primitives the encoder

@@ -104,2 +131,5 @@ * layer dispatches into; callers reach the underlying `Buffer` only when they

if (needed > this.buffer.length) {
if (needed > MAX_SIZE) {
throw new OverflowError(needed)
}
let newSize = this.buffer.length

@@ -109,3 +139,3 @@ // `*= 2` instead of `<<= 1`: `1073741824 << 1` is negative as int32,

while (newSize < needed) newSize *= 2
this.#resize(newSize)
this.#resize(Math.min(newSize, MAX_SIZE))
}

@@ -460,1 +490,3 @@

module.exports = MsgpackChunk
module.exports.MAX_SIZE = MAX_SIZE
module.exports.OverflowError = OverflowError

@@ -100,2 +100,7 @@ 'use strict'

module.exports = { MsgpackChunk, encode }
module.exports = {
MsgpackChunk,
encode,
MAX_SIZE: MsgpackChunk.MAX_SIZE,
OverflowError: MsgpackChunk.OverflowError,
}

@@ -87,2 +87,5 @@ 'use strict'

get memcached () { return require('../../../datadog-plugin-memcached/src') },
// mercurius is traced under the graphql plugin: its instrumentation opens the
// top-level graphql.request span handled by the graphql CompositePlugin.
get mercurius () { return require('../../../datadog-plugin-graphql/src') },
get 'microgateway-core' () { return require('../../../datadog-plugin-microgateway-core/src') },

@@ -89,0 +92,0 @@ get mocha () { return require('../../../datadog-plugin-mocha/src') },

@@ -11,2 +11,8 @@ 'use strict'

},
// Top-level request span for drivers that funnel through a single entry
// point (mercurius). Matches the cross-tracer `graphql.request` v0 name.
request: {
opName: () => 'graphql.request',
serviceName: identityService,
},
},

@@ -13,0 +19,0 @@ }

@@ -11,2 +11,10 @@ 'use strict'

},
// Top-level request span for drivers that funnel through a single entry
// point (mercurius). Matches the cross-tracer `graphql.server.request` v1
// name. The v1 overlap with the execute span's name above is a known wart
// tracked for a separate cross-tracer unification (breaking) change.
request: {
opName: () => 'graphql.server.request',
serviceName: identityService,
},
},

@@ -13,0 +21,0 @@ }

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display