dd-trace 6.15.0 → 6.16.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/README.md +15 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
- package/ci/test-optimization-validation/package-check.js +4 -1
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
- package/index.d.ts +4 -0
- package/next.d.ts +27 -0
- package/next.js +3 -0
- package/package.json +15 -11
- package/packages/datadog-esbuild/index.js +14 -1
- package/packages/datadog-esbuild/src/resolver.js +284 -0
- package/packages/datadog-esbuild/src/utils.js +163 -119
- package/packages/datadog-instrumentations/src/ai.js +39 -97
- package/packages/datadog-instrumentations/src/anthropic.js +76 -134
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
- package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
- package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
- package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
- package/packages/datadog-instrumentations/src/bullmq.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
- package/packages/datadog-instrumentations/src/graphql.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
- package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
- package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
- package/packages/datadog-instrumentations/src/langchain.js +1 -1
- package/packages/datadog-instrumentations/src/langgraph.js +1 -1
- package/packages/datadog-instrumentations/src/mercurius.js +1 -1
- package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
- package/packages/datadog-instrumentations/src/openai.js +54 -76
- package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
- package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
- package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
- package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
- package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
- package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
- package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
- package/packages/datadog-turbopack/index.js +212 -0
- package/packages/datadog-turbopack/src/constants.js +5 -0
- package/packages/datadog-turbopack/src/loader.js +500 -0
- package/packages/dd-trace/src/aiguard/index.js +2 -1
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
- package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
- package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
- package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
- package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
- package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
- package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/helper.js +10 -2
- package/packages/dd-trace/src/config/index.js +20 -5
- package/packages/dd-trace/src/config/supported-configurations.json +8 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
- package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
- package/packages/dd-trace/src/evp_proxy/path.js +12 -3
- package/packages/dd-trace/src/exporters/common/docker.js +4 -1
- package/packages/dd-trace/src/exporters/common/proxy.js +3 -0
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +7 -1
- package/packages/dd-trace/src/plugin_manager.js +9 -3
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -1
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +0 -10
|
@@ -8,46 +8,28 @@ const ch = dc.tracingChannel('apm:openai:request')
|
|
|
8
8
|
const onStreamedChunkCh = dc.channel('apm:openai:request:chunk')
|
|
9
9
|
const responsePromiseContexts = new WeakMap()
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const responsesAfterChannel = dc.channel('dd-trace:openai:responses:after')
|
|
18
|
-
|
|
19
|
-
const LIFECYCLE_CHANNELS = {
|
|
20
|
-
'chat.completions': {
|
|
21
|
-
before: chatCompletionsBeforeChannel,
|
|
22
|
-
after: chatCompletionsAfterChannel,
|
|
23
|
-
},
|
|
24
|
-
responses: {
|
|
25
|
-
before: responsesBeforeChannel,
|
|
26
|
-
after: responsesAfterChannel,
|
|
27
|
-
},
|
|
28
|
-
}
|
|
11
|
+
// Published synchronously with the native call data. A subscriber may set either callback and
|
|
12
|
+
// this instrumentation applies it, so the wrapping stays here and only the policy lives outside:
|
|
13
|
+
// beforeResult () => Promise<void>|undefined holds the result back until it settles
|
|
14
|
+
// onResult (body) => unknown|Promise<unknown> inspects or replaces the delivered body
|
|
15
|
+
const chatCompletionsInterceptChannel = dc.channel('dd-trace:openai:chat.completions:intercept')
|
|
16
|
+
const responsesInterceptChannel = dc.channel('dd-trace:openai:responses:intercept')
|
|
29
17
|
|
|
30
18
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Subscribers push async work into `pending` synchronously during publication and
|
|
34
|
-
* abort `abortController` with an error before the pushed promise resolves to block.
|
|
19
|
+
* Returns `promise`, but not before `settled` settles, so a rejection from an interceptor
|
|
20
|
+
* reaches the caller instead of the SDK's value.
|
|
35
21
|
*
|
|
36
|
-
* @
|
|
37
|
-
* @param {
|
|
38
|
-
* @
|
|
22
|
+
* @template T
|
|
23
|
+
* @param {Promise<T>} promise
|
|
24
|
+
* @param {Promise<void>|undefined} settled
|
|
25
|
+
* @returns {Promise<T>}
|
|
39
26
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
const ctx = { ...payload, abortController, pending: [] }
|
|
27
|
+
function heldUntil (promise, settled) {
|
|
28
|
+
if (!settled) return promise
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
if (abortController.signal.aborted) {
|
|
48
|
-
throw abortController.signal.reason
|
|
49
|
-
}
|
|
50
|
-
})
|
|
30
|
+
// The interceptor's rejection wins over an earlier SDK rejection, so keep that one handled.
|
|
31
|
+
promise.catch(() => {})
|
|
32
|
+
return settled.then(() => promise)
|
|
51
33
|
}
|
|
52
34
|
|
|
53
35
|
const V4_PACKAGE_SHIMS = [
|
|
@@ -57,6 +39,7 @@ const V4_PACKAGE_SHIMS = [
|
|
|
57
39
|
baseResource: 'chat.completions',
|
|
58
40
|
methods: ['create'],
|
|
59
41
|
streamedResponse: true,
|
|
42
|
+
interceptChannel: chatCompletionsInterceptChannel,
|
|
60
43
|
},
|
|
61
44
|
{
|
|
62
45
|
file: 'resources/completions',
|
|
@@ -72,6 +55,7 @@ const V4_PACKAGE_SHIMS = [
|
|
|
72
55
|
methods: ['create'], // implicitly covers `parse` as well which calls `create` under the hood
|
|
73
56
|
streamedResponse: true,
|
|
74
57
|
versions: ['>=4.87.0'],
|
|
58
|
+
interceptChannel: responsesInterceptChannel,
|
|
75
59
|
},
|
|
76
60
|
{
|
|
77
61
|
file: 'resources/embeddings',
|
|
@@ -185,6 +169,7 @@ const V4_PACKAGE_SHIMS = [
|
|
|
185
169
|
methods: ['create'],
|
|
186
170
|
streamedResponse: true,
|
|
187
171
|
versions: ['>=4.85.0'],
|
|
172
|
+
interceptChannel: chatCompletionsInterceptChannel,
|
|
188
173
|
},
|
|
189
174
|
]
|
|
190
175
|
|
|
@@ -287,13 +272,11 @@ for (const extension of extensions) {
|
|
|
287
272
|
return handleUnwrappedAPIPromise(parsedPromise, state)
|
|
288
273
|
})
|
|
289
274
|
|
|
275
|
+
// Raw-response callers bypass `parse`, so run the interceptor here too.
|
|
290
276
|
shimmer.wrap(prototype, 'asResponse', asResponse => function (...args) {
|
|
291
277
|
const state = responsePromiseContexts.get(this.responsePromise)
|
|
292
|
-
const responsePromise = asResponse.apply(this, args)
|
|
293
|
-
|
|
294
|
-
if (!state?.getBeforeVerdict) return responsePromise
|
|
295
278
|
|
|
296
|
-
return
|
|
279
|
+
return heldUntil(asResponse.apply(this, args), state?.interceptCtx?.beforeResult?.())
|
|
297
280
|
})
|
|
298
281
|
|
|
299
282
|
return exports
|
|
@@ -303,7 +286,7 @@ for (const extension of extensions) {
|
|
|
303
286
|
|
|
304
287
|
for (const extension of extensions) {
|
|
305
288
|
for (const shim of V4_PACKAGE_SHIMS) {
|
|
306
|
-
const { file, targetClass, baseResource, methods, versions, streamedResponse } = shim
|
|
289
|
+
const { file, targetClass, baseResource, methods, versions, streamedResponse, interceptChannel } = shim
|
|
307
290
|
addHook({ name: 'openai', file: file + extension, versions: versions || ['>=4'] }, exports => {
|
|
308
291
|
const targetPrototype = exports[targetClass].prototype
|
|
309
292
|
|
|
@@ -314,10 +297,9 @@ for (const extension of extensions) {
|
|
|
314
297
|
// chat.completions and completions
|
|
315
298
|
const stream = streamedResponse && getOption(args, 'stream', false)
|
|
316
299
|
|
|
317
|
-
const
|
|
318
|
-
const hasLifecycle = !!channels && (channels.before.hasSubscribers || channels.after.hasSubscribers)
|
|
300
|
+
const intercepted = !stream && interceptChannel?.hasSubscribers
|
|
319
301
|
|
|
320
|
-
if (!ch.start.hasSubscribers && !
|
|
302
|
+
if (!ch.start.hasSubscribers && !intercepted) {
|
|
321
303
|
return methodFn.apply(this, args)
|
|
322
304
|
}
|
|
323
305
|
|
|
@@ -330,24 +312,21 @@ for (const extension of extensions) {
|
|
|
330
312
|
}
|
|
331
313
|
|
|
332
314
|
return ch.start.runStores(ctx, () => {
|
|
333
|
-
// Capture the parent span explicitly: the _thenUnwrap/parse path decouples
|
|
334
|
-
// the lazy evaluation from the active scope at call time.
|
|
335
|
-
const parentSpan = hasLifecycle ? ctx.currentStore?.span : undefined
|
|
336
|
-
|
|
337
315
|
const apiProm = methodFn.apply(this, args)
|
|
338
316
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
beforeVerdict ??= publishLifecycle(beforeChannel, { args, parentSpan })
|
|
345
|
-
return beforeVerdict
|
|
346
|
-
}
|
|
347
|
-
: null
|
|
317
|
+
let interceptCtx
|
|
318
|
+
if (intercepted) {
|
|
319
|
+
interceptCtx = { arguments: args, tracingContext: ctx }
|
|
320
|
+
interceptChannel.publish(interceptCtx)
|
|
321
|
+
}
|
|
348
322
|
|
|
349
|
-
|
|
350
|
-
|
|
323
|
+
// Keyed by the response promise rather than the APIPromise: `_thenUnwrap` builds a new
|
|
324
|
+
// APIPromise around the same response, which is how `responses.parse` and the
|
|
325
|
+
// pagination page types reach the prototype wrappers above. A method that resolves to
|
|
326
|
+
// a plain value has none, and a WeakMap rejects a non-object key.
|
|
327
|
+
if (apiProm?.responsePromise) {
|
|
328
|
+
responsePromiseContexts.set(apiProm.responsePromise, { ctx, stream, interceptCtx })
|
|
329
|
+
}
|
|
351
330
|
|
|
352
331
|
ch.end.publish(ctx)
|
|
353
332
|
|
|
@@ -361,20 +340,17 @@ for (const extension of extensions) {
|
|
|
361
340
|
}
|
|
362
341
|
|
|
363
342
|
function handleUnwrappedAPIPromise (apiProm, state) {
|
|
364
|
-
const { ctx, stream,
|
|
365
|
-
const gatedApiProm = getBeforeVerdict
|
|
366
|
-
? Promise.all([getBeforeVerdict(), apiProm]).then(([, result]) => result)
|
|
367
|
-
: apiProm
|
|
343
|
+
const { ctx, stream, interceptCtx } = state
|
|
368
344
|
|
|
369
|
-
return
|
|
345
|
+
return heldUntil(apiProm, interceptCtx?.beforeResult?.())
|
|
370
346
|
.then(([{ response, options }, body]) => {
|
|
371
347
|
if (stream) {
|
|
348
|
+
const wrapIterator = wrapStreamIterator(response, options, ctx)
|
|
349
|
+
|
|
372
350
|
if (body.iterator) {
|
|
373
|
-
shimmer.wrap(body, 'iterator',
|
|
351
|
+
shimmer.wrap(body, 'iterator', wrapIterator)
|
|
374
352
|
} else {
|
|
375
|
-
shimmer.wrap(
|
|
376
|
-
body.response.body, Symbol.asyncIterator, wrapStreamIterator(response, options, ctx)
|
|
377
|
-
)
|
|
353
|
+
shimmer.wrap(body.response.body, Symbol.asyncIterator, wrapIterator)
|
|
378
354
|
}
|
|
379
355
|
return body
|
|
380
356
|
}
|
|
@@ -388,28 +364,29 @@ function handleUnwrappedAPIPromise (apiProm, state) {
|
|
|
388
364
|
},
|
|
389
365
|
}
|
|
390
366
|
|
|
391
|
-
if (!
|
|
367
|
+
if (!interceptCtx?.onResult) {
|
|
392
368
|
finish(ctx, responseData)
|
|
393
369
|
return body
|
|
394
370
|
}
|
|
395
371
|
|
|
396
|
-
// Finish after
|
|
397
|
-
//
|
|
398
|
-
return
|
|
372
|
+
// Finish after the callback settles so a rejection propagates to openai.request and the
|
|
373
|
+
// span wraps its child instead of closing before it.
|
|
374
|
+
return Promise.resolve(interceptCtx.onResult(body)).then(deliveredBody => {
|
|
399
375
|
finish(ctx, responseData)
|
|
400
|
-
return
|
|
376
|
+
return deliveredBody
|
|
401
377
|
})
|
|
402
378
|
})
|
|
403
379
|
.catch(error => {
|
|
404
|
-
|
|
405
|
-
// branch, Before Model block, After Model block, or openai error) — record the error now so
|
|
406
|
-
// the openai.request span is marked errored. If finish already ran, don't double-publish.
|
|
407
|
-
if (!ctx.result) finish(ctx, undefined, error)
|
|
380
|
+
finish(ctx, undefined, error)
|
|
408
381
|
throw error
|
|
409
382
|
})
|
|
410
383
|
}
|
|
411
384
|
|
|
412
385
|
function finish (ctx, response, error) {
|
|
386
|
+
// `parse` is re-entered on every await of the same APIPromise, and a stream may be iterated
|
|
387
|
+
// more than once, so the span must only close once.
|
|
388
|
+
if (ctx.finished) return
|
|
389
|
+
|
|
413
390
|
if (error) {
|
|
414
391
|
ctx.error = error
|
|
415
392
|
ch.error.publish(ctx)
|
|
@@ -419,6 +396,7 @@ function finish (ctx, response, error) {
|
|
|
419
396
|
// so we don't want to override it here
|
|
420
397
|
ctx.result ??= {}
|
|
421
398
|
Object.assign(ctx.result, response)
|
|
399
|
+
ctx.finished = true
|
|
422
400
|
|
|
423
401
|
ch.asyncEnd.publish(ctx)
|
|
424
402
|
}
|
|
@@ -718,7 +718,7 @@ function wrapVitestTestRunner (VitestTestRunner) {
|
|
|
718
718
|
const isAtf = attemptToFixTasks.has(task)
|
|
719
719
|
const isEfd = efdRetryTasks.has(task)
|
|
720
720
|
const shouldTrackStatuses = isEfd || isAtf
|
|
721
|
-
const
|
|
721
|
+
const shouldResetStatus = isEfd || isAtf
|
|
722
722
|
const statuses = isAtf ? attemptToFixTaskToStatuses.get(task) : taskToStatuses.get(task)
|
|
723
723
|
|
|
724
724
|
// These clauses handle task.repeats, whether EFD is enabled or not
|
|
@@ -738,13 +738,10 @@ function wrapVitestTestRunner (VitestTestRunner) {
|
|
|
738
738
|
if (shouldTrackStatuses && statuses) {
|
|
739
739
|
statuses.push(lastExecutionStatus)
|
|
740
740
|
}
|
|
741
|
-
if (
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
745
|
-
// if the last attempt passes, vitest would consider the test as failed
|
|
746
|
-
// but after this change, it will consider the test as passed
|
|
747
|
-
task.result.state = 'pass'
|
|
741
|
+
if (shouldResetStatus) {
|
|
742
|
+
// Reset to Vitest's neutral state so a previous failure does not affect the next repetition.
|
|
743
|
+
// A terminal pass state can leak into reporters before the next repetition determines its status.
|
|
744
|
+
task.result.state = 'run'
|
|
748
745
|
}
|
|
749
746
|
}
|
|
750
747
|
} else if (numRepetition === task.repeats) {
|
|
@@ -759,8 +756,8 @@ function wrapVitestTestRunner (VitestTestRunner) {
|
|
|
759
756
|
} else {
|
|
760
757
|
testPassCh.publish({ task, ...ctx.currentStore })
|
|
761
758
|
}
|
|
762
|
-
if (
|
|
763
|
-
task.result.state = '
|
|
759
|
+
if (shouldResetStatus) {
|
|
760
|
+
task.result.state = 'run'
|
|
764
761
|
}
|
|
765
762
|
}
|
|
766
763
|
|
|
@@ -500,6 +500,8 @@ function extractMessagesFromConverseContent (role, contentBlocks) {
|
|
|
500
500
|
if (block == null || typeof block !== 'object') continue
|
|
501
501
|
if (typeof block.text === 'string') {
|
|
502
502
|
content += block.text
|
|
503
|
+
} else if (typeof block.guardContent?.text?.text === 'string') {
|
|
504
|
+
content += block.guardContent.text.text
|
|
503
505
|
} else if (block.toolUse) {
|
|
504
506
|
toolCalls.push(buildToolCall(block.toolUse))
|
|
505
507
|
} else if (block.toolResult) {
|
|
@@ -611,7 +613,11 @@ function extractRequestParamsConverse (params) {
|
|
|
611
613
|
const prompt = []
|
|
612
614
|
if (params.system) {
|
|
613
615
|
for (const block of params.system) {
|
|
614
|
-
if (typeof block?.text === 'string')
|
|
616
|
+
if (typeof block?.text === 'string') {
|
|
617
|
+
prompt.push({ content: block.text, role: 'system' })
|
|
618
|
+
} else if (typeof block?.guardContent?.text?.text === 'string') {
|
|
619
|
+
prompt.push({ content: block.guardContent.text.text, role: 'system' })
|
|
620
|
+
}
|
|
615
621
|
}
|
|
616
622
|
}
|
|
617
623
|
if (params.messages) {
|
|
@@ -26,7 +26,7 @@ class AzureCosmosPlugin extends DatabasePlugin {
|
|
|
26
26
|
const result = ctx.result
|
|
27
27
|
if (result?.code) span.setTag('db.response.status_code', (result.code).toString())
|
|
28
28
|
if (result?.substatus) span.setTag('cosmosdb.response.sub_status_code', result.substatus)
|
|
29
|
-
|
|
29
|
+
this.finish(ctx)
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
+
const { AUTO_KEEP } = require('../../../ext/priority')
|
|
5
|
+
const { writeTraceparent, writeTracestate } = require('../../dd-trace/src/carrier')
|
|
6
|
+
|
|
7
|
+
const ORCHESTRATION_FAILURE_END_CHANNEL =
|
|
8
|
+
'tracing:orchestrion:durable-functions:TaskOrchestrationExecutor_failure:end'
|
|
9
|
+
const ORCHESTRATOR_COMPLETED_EVENT_TYPE = 13
|
|
4
10
|
|
|
5
11
|
class AzureDurableFunctionsPlugin extends TracingPlugin {
|
|
6
12
|
static get id () { return 'azure-durable-functions' }
|
|
@@ -9,9 +15,35 @@ class AzureDurableFunctionsPlugin extends TracingPlugin {
|
|
|
9
15
|
static get type () { return 'serverless' }
|
|
10
16
|
static get kind () { return 'server' }
|
|
11
17
|
|
|
18
|
+
addTraceSubs () {
|
|
19
|
+
super.addTraceSubs()
|
|
20
|
+
this.addSub(ORCHESTRATION_FAILURE_END_CHANNEL, this.orchestrationFailure)
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
bindStart (ctx) {
|
|
24
|
+
// Continue the trace propagated by the Durable Functions host (W3C traceparent
|
|
25
|
+
// supplied on the invocation's traceContext) so activity/entity invocations join
|
|
26
|
+
// the same trace as the HTTP root instead of each starting a new root.
|
|
27
|
+
let childOf
|
|
28
|
+
if (ctx.traceparent) {
|
|
29
|
+
// extract() returns null when the carrier can't be parsed. Normalize to
|
|
30
|
+
// undefined so startSpan still falls back to any active in-process parent
|
|
31
|
+
// rather than being forced to start a brand new root span.
|
|
32
|
+
const carrier = {}
|
|
33
|
+
|
|
34
|
+
writeTraceparent(carrier, ctx.traceparent)
|
|
35
|
+
if (ctx.tracestate) {
|
|
36
|
+
writeTracestate(carrier, ctx.tracestate)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
childOf = this.tracer.extract('text_map', carrier) ?? undefined
|
|
40
|
+
}
|
|
41
|
+
const extractedAsDrop = childOf?._sampling.priority < AUTO_KEEP
|
|
42
|
+
|
|
13
43
|
const span = this.startSpan(this.operationName(), {
|
|
14
|
-
|
|
44
|
+
startTime: ctx.startTime,
|
|
45
|
+
childOf,
|
|
46
|
+
kind: ctx.trigger === 'Orchestration' ? 'server' : 'internal',
|
|
15
47
|
type: 'serverless',
|
|
16
48
|
|
|
17
49
|
meta: {
|
|
@@ -29,6 +61,19 @@ class AzureDurableFunctionsPlugin extends TracingPlugin {
|
|
|
29
61
|
)
|
|
30
62
|
}
|
|
31
63
|
|
|
64
|
+
// The host clears the W3C sampled flag in traceparent while datadog tracestate
|
|
65
|
+
// still says keep, so extraction would drop this chunk. Re-apply only the propagated
|
|
66
|
+
// `s` priority when it indicates keep, preserving the extracted sampling mechanism.
|
|
67
|
+
if (span._prioritySampler && childOf && extractedAsDrop) {
|
|
68
|
+
const propagatedPriority = propagatedSamplingPriority(childOf._tracestate)
|
|
69
|
+
if (propagatedPriority >= AUTO_KEEP) {
|
|
70
|
+
const spanContext = span.context()
|
|
71
|
+
if (spanContext._parentId === childOf._spanId) {
|
|
72
|
+
spanContext._sampling.priority = propagatedPriority
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
32
77
|
ctx.span = span
|
|
33
78
|
return ctx.currentStore
|
|
34
79
|
}
|
|
@@ -44,6 +89,56 @@ class AzureDurableFunctionsPlugin extends TracingPlugin {
|
|
|
44
89
|
asyncStart (ctx) {
|
|
45
90
|
super.finish(ctx)
|
|
46
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Tags the active initial orchestration span or creates a failure-only span after
|
|
95
|
+
* a resumed orchestration throws. The executor publishes synchronously before
|
|
96
|
+
* Durable Functions serializes the error, so this does not add a promise to
|
|
97
|
+
* orchestration activations.
|
|
98
|
+
*
|
|
99
|
+
* @param {{ arguments?: unknown[], error?: unknown }} executorCtx
|
|
100
|
+
* @returns {void}
|
|
101
|
+
*/
|
|
102
|
+
orchestrationFailure (executorCtx) {
|
|
103
|
+
const args = executorCtx?.arguments
|
|
104
|
+
if (!Array.isArray(args)) return
|
|
105
|
+
|
|
106
|
+
const invocationContext = args[0]
|
|
107
|
+
const history = args[1]
|
|
108
|
+
if (!Array.isArray(history)) return
|
|
109
|
+
|
|
110
|
+
const hasPreviousActivation = history.some(
|
|
111
|
+
event => event?.EventType === ORCHESTRATOR_COMPLETED_EVENT_TYPE
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if (!hasPreviousActivation) {
|
|
115
|
+
this.addError(executorCtx.error)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const traceContext = invocationContext?.traceContext
|
|
120
|
+
const ctx = {
|
|
121
|
+
trigger: 'Orchestration',
|
|
122
|
+
functionName: invocationContext?.functionName,
|
|
123
|
+
traceparent: traceContext?.traceParent,
|
|
124
|
+
tracestate: traceContext?.traceState,
|
|
125
|
+
error: executorCtx.error,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.bindStart(ctx)
|
|
129
|
+
this.error(ctx)
|
|
130
|
+
super.finish(ctx)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Read the datadog-propagated sampling priority (`dd=...;s:<n>`) from a W3C
|
|
135
|
+
// tracestate. Returns undefined when there is no datadog tracestate or no valid
|
|
136
|
+
// `s` value, so callers can distinguish "no propagated decision" from a drop.
|
|
137
|
+
function propagatedSamplingPriority (tracestate) {
|
|
138
|
+
return tracestate?.forVendor('dd', state => {
|
|
139
|
+
const priority = Number(state.get('s'))
|
|
140
|
+
return Number.isInteger(priority) ? priority : undefined
|
|
141
|
+
})
|
|
47
142
|
}
|
|
48
143
|
|
|
49
144
|
module.exports = AzureDurableFunctionsPlugin
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const { getMessageSize } = require('../../dd-trace/src/datastreams')
|
|
4
4
|
const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
|
|
5
|
-
const SpanContext = require('../../dd-trace/src/opentracing/span_context')
|
|
6
|
-
const id = require('../../dd-trace/src/id')
|
|
7
5
|
const { storage } = require('../../datadog-core')
|
|
6
|
+
const reconstructPubSubRequestContext = require('./pubsub-request-context')
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* PULL SUBSCRIPTION: Service explicitly pulls messages from Pub/Sub and processes them.
|
|
@@ -75,27 +74,6 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
|
|
|
75
74
|
})
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
#reconstructPubSubRequestContext (attrs) {
|
|
79
|
-
const traceIdLower = attrs['_dd.pubsub_request.trace_id']
|
|
80
|
-
const spanId = attrs['_dd.pubsub_request.span_id']
|
|
81
|
-
const traceIdUpper = attrs['_dd.pubsub_request.p.tid']
|
|
82
|
-
|
|
83
|
-
if (!traceIdLower || !spanId) return null
|
|
84
|
-
|
|
85
|
-
const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0')
|
|
86
|
-
const traceId = id(traceId128, 16)
|
|
87
|
-
const parentId = id(spanId, 16)
|
|
88
|
-
|
|
89
|
-
const tags = {}
|
|
90
|
-
if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper
|
|
91
|
-
|
|
92
|
-
return new SpanContext({
|
|
93
|
-
traceId,
|
|
94
|
-
spanId: parentId,
|
|
95
|
-
tags,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
77
|
start (ctx) {
|
|
100
78
|
if (!this.config.dsmEnabled) return
|
|
101
79
|
const { message } = ctx
|
|
@@ -128,7 +106,7 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
|
|
|
128
106
|
|
|
129
107
|
const isFirstMessage = batchIndex === 0
|
|
130
108
|
if (isFirstMessage && batchRequestSpanId) {
|
|
131
|
-
const pubsubRequestContext =
|
|
109
|
+
const pubsubRequestContext = reconstructPubSubRequestContext(message.attributes)
|
|
132
110
|
if (pubsubRequestContext) {
|
|
133
111
|
childOf = pubsubRequestContext
|
|
134
112
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
-
const SpanContext = require('../../dd-trace/src/opentracing/span_context')
|
|
5
|
-
const id = require('../../dd-trace/src/id')
|
|
6
4
|
const log = require('../../dd-trace/src/log')
|
|
5
|
+
const reconstructPubSubRequestContext = require('./pubsub-request-context')
|
|
7
6
|
|
|
8
7
|
// WeakMap to track push receive spans by request
|
|
9
8
|
const pushReceiveSpans = new WeakMap()
|
|
@@ -69,7 +68,7 @@ class GoogleCloudPubsubPushSubscriptionPlugin extends TracingPlugin {
|
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
const originalContext = this.#extractContext(messageData)
|
|
72
|
-
const pubsubRequestContext =
|
|
71
|
+
const pubsubRequestContext = reconstructPubSubRequestContext(messageData.attrs) || originalContext
|
|
73
72
|
const isSameTrace = pubsubRequestContext &&
|
|
74
73
|
originalContext?.toTraceId() === pubsubRequestContext.toTraceId()
|
|
75
74
|
|
|
@@ -109,39 +108,6 @@ class GoogleCloudPubsubPushSubscriptionPlugin extends TracingPlugin {
|
|
|
109
108
|
return this.tracer.extract('text_map', messageData.attrs)
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
#reconstructPubSubContext (attrs) {
|
|
113
|
-
/**
|
|
114
|
-
* Reconstruct the batch publish span context from message attributes.
|
|
115
|
-
*
|
|
116
|
-
* When a batch is published, the producer injects:
|
|
117
|
-
* - _dd.pubsub_request.trace_id: lower 64 bits of the batch span's trace ID (hex)
|
|
118
|
-
* - _dd.pubsub_request.span_id: the batch span's span ID (hex)
|
|
119
|
-
* - _dd.pubsub_request.p.tid: upper 64 bits of trace ID (hex, optional for 128-bit traces)
|
|
120
|
-
*
|
|
121
|
-
* This context represents the "pubsub.request" span on the producer side.
|
|
122
|
-
* We use it to create span links, connecting each pubsub.push.receive span back to the original batch.
|
|
123
|
-
*/
|
|
124
|
-
const traceIdLower = attrs['_dd.pubsub_request.trace_id']
|
|
125
|
-
const spanId = attrs['_dd.pubsub_request.span_id']
|
|
126
|
-
const traceIdUpper = attrs['_dd.pubsub_request.p.tid']
|
|
127
|
-
|
|
128
|
-
if (!traceIdLower || !spanId) return null
|
|
129
|
-
|
|
130
|
-
// Reconstruct full 128-bit trace ID (or pad 64-bit to 128-bit)
|
|
131
|
-
const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0')
|
|
132
|
-
const traceId = id(traceId128, 16)
|
|
133
|
-
const parentId = id(spanId, 16)
|
|
134
|
-
|
|
135
|
-
const tags = {}
|
|
136
|
-
if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper
|
|
137
|
-
|
|
138
|
-
return new SpanContext({
|
|
139
|
-
traceId,
|
|
140
|
-
spanId: parentId,
|
|
141
|
-
tags,
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
|
|
145
111
|
#createPushReceiveSpan (messageData, parentContext, linkContext) {
|
|
146
112
|
const { message, subscription, topicName, attrs } = messageData
|
|
147
113
|
const subscriptionName = subscription?.slice(subscription.lastIndexOf('/') + 1) ?? subscription
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const SpanContext = require('../../dd-trace/src/opentracing/span_context')
|
|
4
|
+
const id = require('../../dd-trace/src/id')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Record<string, string>} attributes
|
|
8
|
+
* @returns {import('../../dd-trace/src/opentracing/span_context') | null}
|
|
9
|
+
*/
|
|
10
|
+
function reconstructPubSubRequestContext (attributes) {
|
|
11
|
+
const traceIdLower = attributes['_dd.pubsub_request.trace_id']
|
|
12
|
+
const spanId = attributes['_dd.pubsub_request.span_id']
|
|
13
|
+
const traceIdUpper = attributes['_dd.pubsub_request.p.tid']
|
|
14
|
+
|
|
15
|
+
if (!traceIdLower || !spanId) return null
|
|
16
|
+
|
|
17
|
+
const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0')
|
|
18
|
+
const traceId = id(traceId128, 16)
|
|
19
|
+
const parentId = id(spanId, 16)
|
|
20
|
+
|
|
21
|
+
const tags = {}
|
|
22
|
+
if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper
|
|
23
|
+
|
|
24
|
+
return new SpanContext({
|
|
25
|
+
traceId,
|
|
26
|
+
spanId: parentId,
|
|
27
|
+
tags,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = reconstructPubSubRequestContext
|
|
@@ -61,28 +61,50 @@ class GraphQLToolsResolveErrorPlugin extends TracingPlugin {
|
|
|
61
61
|
* @param {readonly object[]} [fieldNodes]
|
|
62
62
|
*/
|
|
63
63
|
function recordResolveErrorForPath (error, path, fieldNodes) {
|
|
64
|
-
const fieldNode = fieldNodes?.[0]
|
|
65
64
|
const rootCtx = legacyStorage.getStore()?.graphqlRootCtx
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
}
|
|
65
|
+
if (!rootCtx?.config.collapse || !rootCtx.fields) return
|
|
66
|
+
|
|
67
|
+
const field = findResolveField(rootCtx.fields, getPathString(path), fieldNodes?.[0])
|
|
68
|
+
if (field) recordResolveError(field, error)
|
|
72
69
|
}
|
|
73
70
|
|
|
74
71
|
/**
|
|
75
|
-
* @param {string} fieldPath
|
|
76
72
|
* @param {(string | number)[]} path
|
|
77
|
-
* @returns {
|
|
73
|
+
* @returns {string}
|
|
78
74
|
*/
|
|
79
|
-
function
|
|
75
|
+
function getPathString (path) {
|
|
80
76
|
let pathString = ''
|
|
81
77
|
for (let index = 0; index < path.length; index++) {
|
|
82
78
|
if (index !== 0) pathString += '.'
|
|
83
79
|
pathString += typeof path[index] === 'number' ? '*' : path[index]
|
|
84
80
|
}
|
|
85
|
-
return pathString
|
|
81
|
+
return pathString
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {Map<unknown, object>} fields
|
|
86
|
+
* @param {string} pathString
|
|
87
|
+
* @param {object} [fieldNode]
|
|
88
|
+
* @returns {object | undefined}
|
|
89
|
+
*/
|
|
90
|
+
function findResolveField (fields, pathString, fieldNode) {
|
|
91
|
+
const field = fields.get(pathString)
|
|
92
|
+
if (field === undefined) return
|
|
93
|
+
|
|
94
|
+
const parentTypeFields = field.parentTypeFields
|
|
95
|
+
if (parentTypeFields === undefined) {
|
|
96
|
+
return fieldNode === undefined || field.fieldNode === fieldNode ? field : undefined
|
|
97
|
+
}
|
|
98
|
+
if (parentTypeFields.parentTypeName !== undefined) {
|
|
99
|
+
if (fieldNode === undefined || parentTypeFields.fieldNode === fieldNode) return parentTypeFields
|
|
100
|
+
return field.fieldNode === fieldNode ? field : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let matchingField
|
|
104
|
+
for (const candidate of parentTypeFields.values()) {
|
|
105
|
+
if (fieldNode === undefined || candidate.fieldNode === fieldNode) matchingField = candidate
|
|
106
|
+
}
|
|
107
|
+
return matchingField
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
/**
|