dd-trace 5.133.0 → 5.134.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ci/vitest-no-worker-init-runner-setup.mjs +27 -0
- package/index.d.ts +16 -5
- package/package.json +17 -16
- package/packages/datadog-instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/cucumber.js +17 -8
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +9 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/jest.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/vitest.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +5 -0
- package/packages/datadog-instrumentations/src/jest.js +69 -57
- package/packages/datadog-instrumentations/src/mocha/utils.js +35 -2
- package/packages/datadog-instrumentations/src/nova-sonic/session.js +25 -16
- package/packages/datadog-instrumentations/src/playwright.js +2 -0
- package/packages/datadog-instrumentations/src/stripe.js +23 -12
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +7 -3
- package/packages/datadog-instrumentations/src/vitest-util.js +5 -2
- package/packages/datadog-instrumentations/src/vitest.js +8 -0
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +3 -0
- package/packages/datadog-plugin-cypress/src/support.js +2 -0
- package/packages/dd-trace/src/aiguard/evaluation.js +143 -1
- package/packages/dd-trace/src/appsec/iast/path-line.js +2 -1
- package/packages/dd-trace/src/appsec/stack_trace.js +3 -2
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +3 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/index.js +45 -0
- package/packages/dd-trace/src/config/supported-configurations.json +9 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +6 -5
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +5 -5
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +4 -6
- package/packages/dd-trace/src/debugger/unsupported-runtime.js +16 -0
- package/packages/dd-trace/src/llmobs/agent-manifest.js +19 -2
- package/packages/dd-trace/src/llmobs/constants/tags.js +3 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +9 -1
- package/packages/dd-trace/src/llmobs/sdk.js +15 -4
- package/packages/dd-trace/src/llmobs/span_processor.js +12 -0
- package/packages/dd-trace/src/llmobs/tagger.js +18 -8
- package/packages/dd-trace/src/llmobs/util.js +10 -4
- package/packages/dd-trace/src/openfeature/index.js +6 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +4 -4
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +18 -1
- package/packages/dd-trace/src/plugin_manager.js +1 -2
- package/packages/dd-trace/src/plugins/tracing.js +0 -1
- package/packages/dd-trace/src/plugins/util/git.js +91 -35
- package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -1
- package/packages/dd-trace/src/plugins/util/test.js +2 -0
- package/packages/dd-trace/src/plugins/util/web.js +90 -20
- package/packages/dd-trace/src/span_stats.js +4 -1
- package/packages/dd-trace/src/telemetry/logs/log-collector.js +1 -1
- package/packages/dd-trace/src/util.js +8 -4
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +8 -8
|
@@ -16,6 +16,8 @@ const {
|
|
|
16
16
|
METADATA,
|
|
17
17
|
COST_TAGS,
|
|
18
18
|
AGENT_MANIFEST,
|
|
19
|
+
AGENT_VERSION,
|
|
20
|
+
AGENT_VERSION_TAG_KEY,
|
|
19
21
|
TOOL_DEFINITIONS,
|
|
20
22
|
EXPERIMENT_INPUT,
|
|
21
23
|
EXPERIMENT_OUTPUT,
|
|
@@ -32,6 +34,7 @@ const {
|
|
|
32
34
|
PARENT_ID_KEY,
|
|
33
35
|
PARENT_AGENT_NAME,
|
|
34
36
|
PARENT_AGENT_SPAN_ID,
|
|
37
|
+
PARENT_AGENT_VERSION,
|
|
35
38
|
SESSION_ID,
|
|
36
39
|
NAME,
|
|
37
40
|
INPUT_PROMPT,
|
|
@@ -406,6 +409,15 @@ class LLMObsSpanProcessor {
|
|
|
406
409
|
const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
|
|
407
410
|
if (existingTags) tags = { ...tags, ...existingTags }
|
|
408
411
|
|
|
412
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span)
|
|
413
|
+
// Resolved here because a span can become an agent after registration. An agent without its own version, and
|
|
414
|
+
// any other span, reports the version inherited at registration. Either wins over a user
|
|
415
|
+
// tag of the same name.
|
|
416
|
+
const agentVersion = mlObsTags?.[SPAN_KIND] === 'agent'
|
|
417
|
+
? mlObsTags[AGENT_VERSION] || mlObsTags[PARENT_AGENT_VERSION]
|
|
418
|
+
: mlObsTags?.[PARENT_AGENT_VERSION]
|
|
419
|
+
if (agentVersion) tags[AGENT_VERSION_TAG_KEY] = agentVersion
|
|
420
|
+
|
|
409
421
|
return tags
|
|
410
422
|
}
|
|
411
423
|
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
METADATA,
|
|
20
20
|
COST_TAGS,
|
|
21
21
|
AGENT_MANIFEST,
|
|
22
|
+
AGENT_VERSION,
|
|
22
23
|
METRICS,
|
|
23
24
|
TOOL_DEFINITIONS,
|
|
24
25
|
PARENT_ID_KEY,
|
|
@@ -28,6 +29,7 @@ const {
|
|
|
28
29
|
NAME,
|
|
29
30
|
PARENT_AGENT_NAME,
|
|
30
31
|
PARENT_AGENT_SPAN_ID,
|
|
32
|
+
PARENT_AGENT_VERSION,
|
|
31
33
|
PROPAGATED_PARENT_ID_KEY,
|
|
32
34
|
PROPAGATED_PARENT_AGENT_ID_KEY,
|
|
33
35
|
PROPAGATED_PARENT_AGENT_NAME_KEY,
|
|
@@ -116,6 +118,7 @@ class LLMObsTagger {
|
|
|
116
118
|
kind,
|
|
117
119
|
name,
|
|
118
120
|
integration,
|
|
121
|
+
agentVersion,
|
|
119
122
|
_decorator,
|
|
120
123
|
} = {}) {
|
|
121
124
|
// Do not register it in the map if it does not have an LLMObs span kind.
|
|
@@ -193,6 +196,8 @@ class LLMObsTagger {
|
|
|
193
196
|
storage.getStore()?.agentDeclarations
|
|
194
197
|
)
|
|
195
198
|
if (agentDeclarations) this.#applyAgentDeclarations(span, agentDeclarations)
|
|
199
|
+
// The span's own version wins over the enclosing contexts.
|
|
200
|
+
if (agentVersion) this._setTag(span, AGENT_VERSION, agentVersion)
|
|
196
201
|
|
|
197
202
|
// apply annotation context name
|
|
198
203
|
const annotationContextName = annotationContext?.name
|
|
@@ -213,15 +218,16 @@ class LLMObsTagger {
|
|
|
213
218
|
|
|
214
219
|
/**
|
|
215
220
|
* Applies the agents declared by the enclosing annotation contexts, outermost first, to every span in the block.
|
|
216
|
-
*
|
|
217
|
-
*
|
|
221
|
+
* Both are emitted only if the span is an agent when it finishes, since some integrations promote a span to an
|
|
222
|
+
* agent after registration.
|
|
218
223
|
*
|
|
219
224
|
* @param {import('../opentracing/span')} span
|
|
220
225
|
* @param {import('./agent-manifest').AgentDeclaration[]} declarations
|
|
221
226
|
*/
|
|
222
227
|
#applyAgentDeclarations (span, declarations) {
|
|
223
228
|
for (const declaration of declarations) {
|
|
224
|
-
this
|
|
229
|
+
if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
|
|
230
|
+
if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
|
|
225
231
|
}
|
|
226
232
|
}
|
|
227
233
|
|
|
@@ -233,7 +239,9 @@ class LLMObsTagger {
|
|
|
233
239
|
*/
|
|
234
240
|
tagAgent (span, agent) {
|
|
235
241
|
const declaration = buildAgentDeclaration(agent)
|
|
236
|
-
if (declaration)
|
|
242
|
+
if (!declaration) return
|
|
243
|
+
if (declaration.version) this._setTag(span, AGENT_VERSION, declaration.version)
|
|
244
|
+
if (declaration.manifest) this.#tagAgentManifestFields(span, declaration.manifest)
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
/**
|
|
@@ -276,8 +284,9 @@ class LLMObsTagger {
|
|
|
276
284
|
|
|
277
285
|
/**
|
|
278
286
|
* Store the nearest agent ancestor on the span so it can be surfaced as
|
|
279
|
-
* `meta.agent_attribution` at finish
|
|
280
|
-
*
|
|
287
|
+
* `meta.agent_attribution` at finish, along with that agent's version, which the agent panel reads
|
|
288
|
+
* off each span to attribute its spend. Resolved once here, at registration, so downstream children
|
|
289
|
+
* inherit it with a single lookup rather than walking the ancestor chain.
|
|
281
290
|
*
|
|
282
291
|
* @param {import('../opentracing/span')} span
|
|
283
292
|
* @param {import('../opentracing/span')} [parent] the LLMObs parent span, if any
|
|
@@ -285,10 +294,10 @@ class LLMObsTagger {
|
|
|
285
294
|
// TODO: spans whose kind changes after registration (e.g. claude-agent-sdk tools promoted to
|
|
286
295
|
// sub-agents) will not retroactively update already-finished children's attribution. Follow up.
|
|
287
296
|
#tagAgentAttribution (span, parent) {
|
|
288
|
-
let name, spanId
|
|
297
|
+
let name, spanId, version
|
|
289
298
|
if (registry.has(parent)) {
|
|
290
299
|
// Local LLMObs parent: attribute to it if it is an agent, else inherit its resolution.
|
|
291
|
-
({ name, spanId } = resolveAgentAttribution(registry.get(parent), parent))
|
|
300
|
+
({ name, spanId, version } = resolveAgentAttribution(registry.get(parent), parent))
|
|
292
301
|
} else if (span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY]) {
|
|
293
302
|
// Distributed LLMObs parent: inherit the nearest agent propagated from upstream. The
|
|
294
303
|
// name may be absent when the upstream hop ran an older SDK or its name was not
|
|
@@ -300,6 +309,7 @@ class LLMObsTagger {
|
|
|
300
309
|
|
|
301
310
|
if (name != null) this._setTag(span, PARENT_AGENT_NAME, name)
|
|
302
311
|
if (spanId != null) this._setTag(span, PARENT_AGENT_SPAN_ID, spanId)
|
|
312
|
+
if (version) this._setTag(span, PARENT_AGENT_VERSION, version)
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
// TODO: similarly for the following `tag` methods,
|
|
@@ -8,8 +8,10 @@ const {
|
|
|
8
8
|
SPAN_KINDS,
|
|
9
9
|
SPAN_KIND,
|
|
10
10
|
NAME,
|
|
11
|
+
AGENT_VERSION,
|
|
11
12
|
PARENT_AGENT_NAME,
|
|
12
13
|
PARENT_AGENT_SPAN_ID,
|
|
14
|
+
PARENT_AGENT_VERSION,
|
|
13
15
|
} = require('./constants/tags')
|
|
14
16
|
|
|
15
17
|
const DECIMAL_TRACE_ID_REGEX = /^\d+$/
|
|
@@ -325,14 +327,18 @@ function agentNameWireSafe (name) {
|
|
|
325
327
|
*
|
|
326
328
|
* @param {Record<string, unknown> | undefined} tags - Registry entry for the parent span.
|
|
327
329
|
* @param {import('../opentracing/span')} [span] - The parent span itself (needed for span id / name).
|
|
328
|
-
* @returns {{ name: string | undefined, spanId: string | undefined }}
|
|
330
|
+
* @returns {{ name: string | undefined, spanId: string | undefined, version: string | undefined }}
|
|
329
331
|
*/
|
|
330
332
|
function resolveAgentAttribution (tags, span) {
|
|
331
|
-
if (!tags) return { name: undefined, spanId: undefined }
|
|
333
|
+
if (!tags) return { name: undefined, spanId: undefined, version: undefined }
|
|
332
334
|
if (tags[SPAN_KIND] === 'agent') {
|
|
333
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
name: tags[NAME] || span._name,
|
|
337
|
+
spanId: span.context().toSpanId(),
|
|
338
|
+
version: tags[AGENT_VERSION] || tags[PARENT_AGENT_VERSION],
|
|
339
|
+
}
|
|
334
340
|
}
|
|
335
|
-
return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID] }
|
|
341
|
+
return { name: tags[PARENT_AGENT_NAME], spanId: tags[PARENT_AGENT_SPAN_ID], version: tags[PARENT_AGENT_VERSION] }
|
|
336
342
|
}
|
|
337
343
|
|
|
338
344
|
function spanHasError (span) {
|
|
@@ -33,6 +33,12 @@ function _handleFlush () {
|
|
|
33
33
|
* @param {import('../config')} config - Tracer configuration object
|
|
34
34
|
*/
|
|
35
35
|
function enable (config) {
|
|
36
|
+
if (config.featureFlags?.DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED === false) {
|
|
37
|
+
log.debug('Feature Flags: Datadog exposure logging disabled; set %s=true to enable',
|
|
38
|
+
'DD_FEATURE_FLAGS_EXPOSURES_DATADOG_LOGGING_ENABLED')
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
if (exposuresWriter) {
|
|
37
43
|
log.warn('%s already enabled', exposuresWriter.constructor.name)
|
|
38
44
|
return
|
|
@@ -31,14 +31,14 @@ const deliveryStrategies = new WeakMap()
|
|
|
31
31
|
let missingRouteWarningLogged = false
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Logs the unavailable
|
|
34
|
+
* Logs the unavailable event-delivery warning once.
|
|
35
35
|
*
|
|
36
36
|
*/
|
|
37
|
-
function
|
|
37
|
+
function warnEventDeliveryUnavailable () {
|
|
38
38
|
if (missingRouteWarningLogged) return
|
|
39
39
|
missingRouteWarningLogged = true
|
|
40
40
|
logger.warn(
|
|
41
|
-
'Feature Flags
|
|
41
|
+
'Feature Flags event delivery is disabled because no compatible local EVP route or direct intake ' +
|
|
42
42
|
'credentials are available.'
|
|
43
43
|
)
|
|
44
44
|
}
|
|
@@ -169,7 +169,7 @@ function setAgentlessStrategy (config, setWriterEnabledValue) {
|
|
|
169
169
|
if (error) {
|
|
170
170
|
logger.debug('FFE Writer disabled - error getting local receiver info: %s', error.message)
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
warnEventDeliveryUnavailable()
|
|
173
173
|
markUnavailable()
|
|
174
174
|
})
|
|
175
175
|
}
|
|
@@ -95,6 +95,11 @@ const DD_ERROR_META_KEYS = new Set(['error.message'])
|
|
|
95
95
|
class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
96
96
|
#otelTraceSemanticsEnabled
|
|
97
97
|
|
|
98
|
+
// Span meta entries with the same key and value as a resource attribute are already carried by
|
|
99
|
+
// the resource (e.g. DD_TAGS), so they are not repeated as span attributes.
|
|
100
|
+
/** @type {import('@opentelemetry/api').Attributes} */
|
|
101
|
+
#resourceAttributes
|
|
102
|
+
|
|
98
103
|
/**
|
|
99
104
|
* Creates a new OtlpTraceTransformer instance.
|
|
100
105
|
*
|
|
@@ -104,6 +109,16 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
104
109
|
constructor (resourceAttributes, otelTraceSemanticsEnabled) {
|
|
105
110
|
super(resourceAttributes, 'http/json', 'traces')
|
|
106
111
|
this.#otelTraceSemanticsEnabled = otelTraceSemanticsEnabled
|
|
112
|
+
this.#resourceAttributes = resourceAttributes
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @override
|
|
117
|
+
* @param {import('@opentelemetry/api').Attributes} resourceAttributes - Resource attributes
|
|
118
|
+
*/
|
|
119
|
+
updateResourceAttributes (resourceAttributes) {
|
|
120
|
+
super.updateResourceAttributes(resourceAttributes)
|
|
121
|
+
this.#resourceAttributes = resourceAttributes
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
/**
|
|
@@ -216,11 +231,13 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
216
231
|
}
|
|
217
232
|
}
|
|
218
233
|
|
|
219
|
-
// Add meta string tags, skipping keys that map to dedicated OTLP fields
|
|
234
|
+
// Add meta string tags, skipping keys that map to dedicated OTLP fields and tags the
|
|
235
|
+
// resource already carries with the same value (e.g. global tags from DD_TAGS)
|
|
220
236
|
if (span.meta) {
|
|
221
237
|
for (const [key, value] of Object.entries(span.meta)) {
|
|
222
238
|
if (EXCLUDED_META_KEYS.has(key)) continue
|
|
223
239
|
if (this.#otelTraceSemanticsEnabled && DD_ERROR_META_KEYS.has(key)) continue
|
|
240
|
+
if (this.#resourceAttributes[key] === value) continue
|
|
224
241
|
attributes.push({ key, value: { stringValue: value } })
|
|
225
242
|
}
|
|
226
243
|
}
|
|
@@ -102,8 +102,7 @@ module.exports = class PluginManager {
|
|
|
102
102
|
loadPlugin (name) {
|
|
103
103
|
const Plugin = pluginClasses[name]
|
|
104
104
|
|
|
105
|
-
if (!Plugin) return
|
|
106
|
-
if (!this._tracerConfig) return // TODO: don't wait for tracer to be initialized
|
|
105
|
+
if (!Plugin || !this._tracerConfig) return // TODO: don't wait for tracer to be initialized
|
|
107
106
|
|
|
108
107
|
// Check if this is a Test Optimization plugin and Test Optimization is not enabled
|
|
109
108
|
if (TEST_OPTIMIZATION_PLUGINS.has(name) && !this._tracerConfig.isCiVisibility) {
|
|
@@ -267,7 +267,6 @@ class TracingPlugin extends Plugin {
|
|
|
267
267
|
analyticsSampler.sample(span, config.measured)
|
|
268
268
|
|
|
269
269
|
// TODO: Remove this after migration to TracingChannel is done.
|
|
270
|
-
// eslint-disable-next-line unicorn/no-unnecessary-boolean-comparison -- Context objects use the other branch.
|
|
271
270
|
if (enterOrCtx === true) {
|
|
272
271
|
legacyStorage.enterWith({ ...store, span })
|
|
273
272
|
} else if (enterOrCtx) {
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
const os = require('os')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const fs = require('fs')
|
|
6
|
+
const { execFileSync } = require('node:child_process')
|
|
6
7
|
|
|
7
8
|
const log = require('../../log')
|
|
9
|
+
const { getEnvironmentVariables } = require('../../config/helper')
|
|
8
10
|
const {
|
|
9
11
|
incrementCountMetric,
|
|
10
12
|
distributionMetric,
|
|
@@ -49,7 +51,8 @@ function sanitizedExec (
|
|
|
49
51
|
operationMetric,
|
|
50
52
|
durationMetric,
|
|
51
53
|
errorMetric,
|
|
52
|
-
shouldTrim = true
|
|
54
|
+
shouldTrim = true,
|
|
55
|
+
executionOptions
|
|
53
56
|
) {
|
|
54
57
|
return legacyStorage.run({ noop: true }, () => {
|
|
55
58
|
let startTime
|
|
@@ -60,7 +63,14 @@ function sanitizedExec (
|
|
|
60
63
|
startTime = Date.now()
|
|
61
64
|
}
|
|
62
65
|
try {
|
|
63
|
-
|
|
66
|
+
const exec = executionOptions?.uncached ? execFileSync : cachedExec
|
|
67
|
+
/** @type {import('node:child_process').ExecFileSyncOptions} */
|
|
68
|
+
const options = { stdio: 'pipe' }
|
|
69
|
+
if (executionOptions?.timeout) options.timeout = executionOptions.timeout
|
|
70
|
+
if (executionOptions?.localOnly) {
|
|
71
|
+
options.env = { ...getEnvironmentVariables(), GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '' }
|
|
72
|
+
}
|
|
73
|
+
let result = exec('git', flags, options).toString()
|
|
64
74
|
|
|
65
75
|
if (shouldTrim) {
|
|
66
76
|
result = result.replaceAll(/(\r\n|\n|\r)/g, '')
|
|
@@ -79,6 +89,7 @@ function sanitizedExec (
|
|
|
79
89
|
})
|
|
80
90
|
}
|
|
81
91
|
log.error('Git plugin error executing command', err)
|
|
92
|
+
if (executionOptions?.throwOnError) throw err
|
|
82
93
|
return ''
|
|
83
94
|
}
|
|
84
95
|
})
|
|
@@ -103,12 +114,14 @@ function isGitAvailable () {
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
function isShallowRepository () {
|
|
117
|
+
function isShallowRepository (uncached = false) {
|
|
107
118
|
return sanitizedExec(
|
|
108
119
|
['rev-parse', '--is-shallow-repository'],
|
|
109
120
|
{ name: TELEMETRY_GIT_COMMAND, tags: { command: 'check_shallow' } },
|
|
110
121
|
{ name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'check_shallow' } },
|
|
111
|
-
{ name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'check_shallow' } }
|
|
122
|
+
{ name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'check_shallow' } },
|
|
123
|
+
true,
|
|
124
|
+
{ uncached }
|
|
112
125
|
) === 'true'
|
|
113
126
|
}
|
|
114
127
|
|
|
@@ -223,6 +236,13 @@ function getLatestCommits () {
|
|
|
223
236
|
}
|
|
224
237
|
|
|
225
238
|
function getGitDiff (baseCommit, targetCommit) {
|
|
239
|
+
// Fetch the required snapshots explicitly; a depth-1 metadata fetch does not provide their ancestry.
|
|
240
|
+
for (const commit of targetCommit ? [baseCommit, targetCommit] : [baseCommit]) {
|
|
241
|
+
if (!hasLocalGitObject(commit)) {
|
|
242
|
+
// Use the current checkout state so a stale cache cannot shorten a complete clone.
|
|
243
|
+
fetchHeadCommitSha(commit, isShallowRepository(true))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
226
246
|
const flags = ['diff', '-U0', '--word-diff=porcelain', baseCommit]
|
|
227
247
|
if (targetCommit) {
|
|
228
248
|
flags.push(targetCommit)
|
|
@@ -232,7 +252,8 @@ function getGitDiff (baseCommit, targetCommit) {
|
|
|
232
252
|
{ name: TELEMETRY_GIT_COMMAND, tags: { command: 'diff' } },
|
|
233
253
|
{ name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'diff' } },
|
|
234
254
|
{ name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'diff' } },
|
|
235
|
-
false // important not to trim or we'll lose the line breaks which we need to detect impacted tests
|
|
255
|
+
false, // important not to trim or we'll lose the line breaks which we need to detect impacted tests
|
|
256
|
+
{ uncached: true }
|
|
236
257
|
)
|
|
237
258
|
}
|
|
238
259
|
|
|
@@ -464,6 +485,7 @@ function getGitMetadata (ciMetadata) {
|
|
|
464
485
|
authorEmail: ciAuthorEmail,
|
|
465
486
|
ciWorkspacePath,
|
|
466
487
|
headCommitSha,
|
|
488
|
+
pullRequestBaseBranchSha,
|
|
467
489
|
} = ciMetadata
|
|
468
490
|
|
|
469
491
|
// With stdio: 'pipe', errors in this command will not be output to the parent process,
|
|
@@ -486,8 +508,25 @@ function getGitMetadata (ciMetadata) {
|
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
if (headCommitSha) {
|
|
489
|
-
|
|
490
|
-
|
|
511
|
+
const shallow = isShallowRepository(true)
|
|
512
|
+
const fetchedAtDepthOne = shallow && fetchHeadCommitSha(headCommitSha)
|
|
513
|
+
const headFlags = [
|
|
514
|
+
'show',
|
|
515
|
+
'-s',
|
|
516
|
+
'--format=\'%H","%aI","%an","%ae","%cI","%cn","%ce","%B\'',
|
|
517
|
+
headCommitSha,
|
|
518
|
+
]
|
|
519
|
+
// Only explicit, bounded fetches may contact the remote during head metadata recovery.
|
|
520
|
+
const headOptions = { uncached: true, localOnly: true }
|
|
521
|
+
let headInfo = sanitizedExec(headFlags, null, null, null, false, headOptions)
|
|
522
|
+
if (!headInfo && (fetchedAtDepthOne || !shallow)) {
|
|
523
|
+
fetchHeadCommitSha(headCommitSha, false)
|
|
524
|
+
headInfo = sanitizedExec(headFlags, null, null, null, false, headOptions)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Prepare a known diff base while the remote is available, including when Git upload is disabled.
|
|
528
|
+
if (shallow && pullRequestBaseBranchSha && !hasLocalGitObject(pullRequestBaseBranchSha)) {
|
|
529
|
+
fetchHeadCommitSha(pullRequestBaseBranchSha)
|
|
491
530
|
}
|
|
492
531
|
|
|
493
532
|
const [
|
|
@@ -499,18 +538,7 @@ function getGitMetadata (ciMetadata) {
|
|
|
499
538
|
headCommitterName,
|
|
500
539
|
headCommitterEmail,
|
|
501
540
|
headCommitMessage,
|
|
502
|
-
] =
|
|
503
|
-
[
|
|
504
|
-
'show',
|
|
505
|
-
'-s',
|
|
506
|
-
'--format=\'%H","%aI","%an","%ae","%cI","%cn","%ce","%B\'',
|
|
507
|
-
headCommitSha,
|
|
508
|
-
],
|
|
509
|
-
null,
|
|
510
|
-
null,
|
|
511
|
-
null,
|
|
512
|
-
false
|
|
513
|
-
).split('","')
|
|
541
|
+
] = headInfo.split('","')
|
|
514
542
|
|
|
515
543
|
if (gitHeadCommitSha) {
|
|
516
544
|
tags[GIT_COMMIT_HEAD_AUTHOR_DATE] = headAuthorDate
|
|
@@ -558,23 +586,51 @@ function getGitInformationDiscrepancy () {
|
|
|
558
586
|
return { gitRepositoryUrl, gitCommitSHA }
|
|
559
587
|
}
|
|
560
588
|
|
|
561
|
-
|
|
562
|
-
|
|
589
|
+
/** @param {string} sha */
|
|
590
|
+
function hasLocalGitObject (sha) {
|
|
591
|
+
return legacyStorage.run({ noop: true }, () => {
|
|
592
|
+
try {
|
|
593
|
+
execFileSync('git', ['cat-file', '-e', sha], {
|
|
594
|
+
stdio: 'pipe',
|
|
595
|
+
// Older Git versions ignore GIT_NO_LAZY_FETCH, so also disallow transports for this local-only probe.
|
|
596
|
+
env: { ...getEnvironmentVariables(), GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '' },
|
|
597
|
+
})
|
|
598
|
+
return true
|
|
599
|
+
} catch {
|
|
600
|
+
return false
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
}
|
|
563
604
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
605
|
+
function fetchHeadCommitSha (headSha, useDepth = true) {
|
|
606
|
+
const remoteName = getGitRemoteName()
|
|
607
|
+
// Existing heads retain the original repair/history behavior; a failed bounded fetch also retries that path.
|
|
608
|
+
const depths = useDepth && !hasLocalGitObject(headSha) ? [true, false] : [false]
|
|
609
|
+
for (const shallow of depths) {
|
|
610
|
+
try {
|
|
611
|
+
sanitizedExec(
|
|
612
|
+
[
|
|
613
|
+
'fetch',
|
|
614
|
+
...(shallow ? ['--depth=1'] : []),
|
|
615
|
+
'--update-shallow',
|
|
616
|
+
'--filter=blob:none',
|
|
617
|
+
'--recurse-submodules=no',
|
|
618
|
+
'--no-write-fetch-head',
|
|
619
|
+
remoteName,
|
|
620
|
+
headSha,
|
|
621
|
+
],
|
|
622
|
+
{ name: TELEMETRY_GIT_COMMAND, tags: { command: 'fetch_head_commit_sha' } },
|
|
623
|
+
{ name: TELEMETRY_GIT_COMMAND_MS, tags: { command: 'fetch_head_commit_sha' } },
|
|
624
|
+
{ name: TELEMETRY_GIT_COMMAND_ERRORS, tags: { command: 'fetch_head_commit_sha' } },
|
|
625
|
+
true,
|
|
626
|
+
{ uncached: true, throwOnError: true, timeout: 5000 }
|
|
627
|
+
)
|
|
628
|
+
return shallow
|
|
629
|
+
} catch {
|
|
630
|
+
// sanitizedExec records the failure; retry without a depth limit if the bounded fetch failed.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return false
|
|
578
634
|
}
|
|
579
635
|
|
|
580
636
|
module.exports = {
|
|
@@ -12,8 +12,9 @@ const NODE_MODULES_PATTERN_START = `node_modules${sep}`
|
|
|
12
12
|
* current file path ends with the expected path structure from the repo root.
|
|
13
13
|
* This is needed for local and CI where dd-trace-js is not in node_modules.
|
|
14
14
|
* In production, these frames are already filtered by isNodeModulesFrame.
|
|
15
|
+
* Bundled into ESM output, `__filename` is not defined, and dd-trace is not running from its repo.
|
|
15
16
|
*/
|
|
16
|
-
const SHOULD_FILTER_DD_TRACE_INSTRUMENTAION = __filename.endsWith(
|
|
17
|
+
const SHOULD_FILTER_DD_TRACE_INSTRUMENTAION = typeof __filename === 'string' && __filename.endsWith(
|
|
17
18
|
join('packages', 'dd-trace', 'src', 'plugins', 'util', 'stacktrace.js')
|
|
18
19
|
)
|
|
19
20
|
|
|
@@ -799,6 +799,8 @@ function getTestEnvironmentMetadata (testFramework, config, shouldSkipGitMetadat
|
|
|
799
799
|
commitMessage,
|
|
800
800
|
ciWorkspacePath,
|
|
801
801
|
headCommitSha,
|
|
802
|
+
pullRequestBaseBranchSha: userProvidedGitMetadata[GIT_PULL_REQUEST_BASE_BRANCH_SHA] ||
|
|
803
|
+
ciMetadata[GIT_PULL_REQUEST_BASE_BRANCH_SHA],
|
|
802
804
|
})
|
|
803
805
|
}
|
|
804
806
|
|
|
@@ -321,20 +321,16 @@ const web = {
|
|
|
321
321
|
const writeHead = res.writeHead
|
|
322
322
|
|
|
323
323
|
return function (statusCode, statusMessage, headers) {
|
|
324
|
-
// CORS preflight tagging only matters for OPTIONS requests. Skip the
|
|
325
|
-
// getHeaders() spread + isOriginAllowed work entirely for the common
|
|
326
|
-
// GET / POST / etc. case. Node's http module passes `req.method`
|
|
327
|
-
// through unchanged, so all standard methods are uppercase; the
|
|
328
|
-
// `toLowerCase` fallback covers any non-standard caller.
|
|
329
324
|
if (req.method === 'OPTIONS' || req.method.toLowerCase() === 'options') {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
325
|
+
const headerIndex = typeof statusMessage === 'string' || headers != null ? 2 : 1
|
|
326
|
+
const updatedHeaders = addAllowHeaders(req, res, arguments[headerIndex])
|
|
327
|
+
if (updatedHeaders) {
|
|
328
|
+
arguments[headerIndex] = updatedHeaders
|
|
329
|
+
const result = writeHead.apply(this, arguments)
|
|
330
|
+
if (context.config.headers.length > 0) context.responseHeaders = updatedHeaders
|
|
331
|
+
return result
|
|
335
332
|
}
|
|
336
333
|
}
|
|
337
|
-
|
|
338
334
|
return writeHead.apply(this, arguments)
|
|
339
335
|
}
|
|
340
336
|
},
|
|
@@ -362,8 +358,28 @@ function normalizeHeadersCarrier (headers) {
|
|
|
362
358
|
return carrier
|
|
363
359
|
}
|
|
364
360
|
|
|
361
|
+
/**
|
|
362
|
+
* @param {import('node:http').IncomingMessage} req
|
|
363
|
+
* @param {import('node:http').ServerResponse | import('node:http2').Http2ServerResponse} res
|
|
364
|
+
* @param {ResponseHeaders} headers
|
|
365
|
+
*/
|
|
365
366
|
function addAllowHeaders (req, res, headers) {
|
|
366
|
-
|
|
367
|
+
if (!req.headers.origin || !req.headers['access-control-request-headers'] ||
|
|
368
|
+
res.headersSent || res.stream?.destroyed || res.stream?.closed) return
|
|
369
|
+
|
|
370
|
+
const originKey = findHeader(headers, 'access-control-allow-origin')
|
|
371
|
+
const allowOrigin = originKey === undefined
|
|
372
|
+
? res.getHeader('access-control-allow-origin')
|
|
373
|
+
: getHeaderValue(headers, originKey)
|
|
374
|
+
if (allowOrigin !== '*' && allowOrigin !== req.headers.origin) return
|
|
375
|
+
|
|
376
|
+
const allowKey = findHeader(headers, 'access-control-allow-headers')
|
|
377
|
+
const existing = allowKey === undefined
|
|
378
|
+
? res.getHeader('access-control-allow-headers')
|
|
379
|
+
: getHeaderValue(headers, allowKey)
|
|
380
|
+
if (allowKey !== undefined && existing == null) return
|
|
381
|
+
|
|
382
|
+
const allowHeaders = splitHeader(existing)
|
|
367
383
|
const requestHeaders = splitHeader(req.headers['access-control-request-headers'])
|
|
368
384
|
const contextHeaders = [
|
|
369
385
|
'baggage',
|
|
@@ -384,19 +400,69 @@ function addAllowHeaders (req, res, headers) {
|
|
|
384
400
|
}
|
|
385
401
|
|
|
386
402
|
if (allowHeaders.length > 0) {
|
|
387
|
-
|
|
403
|
+
const value = uniq(allowHeaders).join(',')
|
|
404
|
+
if (headers) return setAllowHeaders(headers, allowKey, value)
|
|
405
|
+
res.setHeader('access-control-allow-headers', value)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** @typedef {import('node:http').OutgoingHttpHeader} HeaderValue */
|
|
410
|
+
/** @typedef {import('node:http').OutgoingHttpHeaders | Array<HeaderValue | [string, HeaderValue]>} ResponseHeaders */
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @param {ResponseHeaders} headers
|
|
414
|
+
* @param {string} name
|
|
415
|
+
*/
|
|
416
|
+
function findHeader (headers, name) {
|
|
417
|
+
if (!headers) return
|
|
418
|
+
if (Array.isArray(headers)) {
|
|
419
|
+
const tuples = Array.isArray(headers[0])
|
|
420
|
+
for (let i = headers.length - (tuples ? 1 : 2); i >= 0; i -= tuples ? 1 : 2) {
|
|
421
|
+
const key = tuples ? headers[i][0] : headers[i]
|
|
422
|
+
if (typeof key === 'string' && key.toLowerCase() === name) return tuples ? i : i + 1
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
const keys = Object.keys(headers)
|
|
426
|
+
for (let i = keys.length - 1; i >= 0; i--) {
|
|
427
|
+
if (keys[i].toLowerCase() === name) return keys[i]
|
|
428
|
+
}
|
|
388
429
|
}
|
|
389
430
|
}
|
|
390
431
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
432
|
+
/**
|
|
433
|
+
* @param {ResponseHeaders} headers
|
|
434
|
+
* @param {string | number} key
|
|
435
|
+
*/
|
|
436
|
+
function getHeaderValue (headers, key) {
|
|
437
|
+
return Array.isArray(headers) && Array.isArray(headers[0]) ? headers[key][1] : headers[key]
|
|
438
|
+
}
|
|
394
439
|
|
|
395
|
-
|
|
440
|
+
/**
|
|
441
|
+
* @param {ResponseHeaders} headers
|
|
442
|
+
* @param {string | number | undefined} key
|
|
443
|
+
* @param {string} value
|
|
444
|
+
*/
|
|
445
|
+
function setAllowHeaders (headers, key, value) {
|
|
446
|
+
if (Array.isArray(headers)) {
|
|
447
|
+
const result = []
|
|
448
|
+
const tuples = Array.isArray(headers[0])
|
|
449
|
+
// Native writeHead ignores custom array iterators.
|
|
450
|
+
for (let i = 0; i < headers.length; i++) result.push(headers[i])
|
|
451
|
+
if (key !== undefined) {
|
|
452
|
+
result[key] = tuples ? [headers[key][0], value] : value
|
|
453
|
+
} else if (tuples) {
|
|
454
|
+
result.push(['access-control-allow-headers', value])
|
|
455
|
+
} else {
|
|
456
|
+
result.push('access-control-allow-headers', value)
|
|
457
|
+
}
|
|
458
|
+
return result
|
|
459
|
+
}
|
|
460
|
+
return { ...headers, [key ?? 'access-control-allow-headers']: value }
|
|
396
461
|
}
|
|
397
462
|
|
|
398
463
|
function splitHeader (str) {
|
|
399
|
-
|
|
464
|
+
if (Array.isArray(str)) str = str.join(',')
|
|
465
|
+
return str === undefined ? [] : String(str).split(',').map((header) => header.trim())
|
|
400
466
|
}
|
|
401
467
|
|
|
402
468
|
function addRequestTags (context, spanType) {
|
|
@@ -527,10 +593,14 @@ function addRequestHeaders (context) {
|
|
|
527
593
|
}
|
|
528
594
|
|
|
529
595
|
function addResponseHeaders (context) {
|
|
530
|
-
const { res, config, span, inferredProxySpan } = context
|
|
596
|
+
const { res, config, span, inferredProxySpan, responseHeaders } = context
|
|
531
597
|
|
|
532
598
|
for (const [key, tag] of config.headers) {
|
|
533
|
-
|
|
599
|
+
let resHeader = res.getHeader(key)
|
|
600
|
+
if (resHeader === undefined && responseHeaders) {
|
|
601
|
+
const headerKey = findHeader(responseHeaders, key)
|
|
602
|
+
if (headerKey !== undefined) resHeader = getHeaderValue(responseHeaders, headerKey)
|
|
603
|
+
}
|
|
534
604
|
if (resHeader) {
|
|
535
605
|
const tagName = tag || `${HTTP_RESPONSE_HEADERS}.${key}`
|
|
536
606
|
span.setTag(tagName, resHeader)
|
|
@@ -208,6 +208,7 @@ class SpanStatsProcessor {
|
|
|
208
208
|
env,
|
|
209
209
|
tags,
|
|
210
210
|
version: appVersion,
|
|
211
|
+
reportHostname,
|
|
211
212
|
_DD_TRACE_METRICS_OTEL_FLUSH_INTERVAL: flushIntervalMs,
|
|
212
213
|
} = config
|
|
213
214
|
if (!otlpExporter) {
|
|
@@ -217,7 +218,9 @@ class SpanStatsProcessor {
|
|
|
217
218
|
this.interval = intervalMs / 1e3
|
|
218
219
|
this.bucketSizeNs = intervalMs * 1e6
|
|
219
220
|
this.buckets = new TimeBuckets(Boolean(otlpExporter))
|
|
220
|
-
|
|
221
|
+
// A non-empty client hostname takes precedence over the agent's own hostname for the resulting
|
|
222
|
+
// metrics, so only send it when explicitly requested.
|
|
223
|
+
this.hostname = reportHostname ? os.hostname() : ''
|
|
221
224
|
this.enabled = enabled
|
|
222
225
|
this.otlpExporter = otlpExporter || null
|
|
223
226
|
this.env = env
|