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.
Files changed (81) hide show
  1. package/README.md +15 -0
  2. package/ci/test-optimization-validation/framework-adapters/cucumber.js +4 -2
  3. package/ci/test-optimization-validation/package-check.js +4 -1
  4. package/ci/test-optimization-validation/scenarios/ci-wiring.js +1 -1
  5. package/index.d.ts +4 -0
  6. package/next.d.ts +27 -0
  7. package/next.js +3 -0
  8. package/package.json +15 -11
  9. package/packages/datadog-esbuild/index.js +14 -1
  10. package/packages/datadog-esbuild/src/resolver.js +284 -0
  11. package/packages/datadog-esbuild/src/utils.js +163 -119
  12. package/packages/datadog-instrumentations/src/ai.js +39 -97
  13. package/packages/datadog-instrumentations/src/anthropic.js +76 -134
  14. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +1 -1
  15. package/packages/datadog-instrumentations/src/azure-cosmos.js +1 -1
  16. package/packages/datadog-instrumentations/src/azure-durable-functions.js +23 -9
  17. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  18. package/packages/datadog-instrumentations/src/bullmq.js +1 -1
  19. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  20. package/packages/datadog-instrumentations/src/dd-trace-api.js +2 -2
  21. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  22. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +39 -14
  23. package/packages/datadog-instrumentations/src/helpers/instrument.js +25 -8
  24. package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +72 -0
  25. package/packages/datadog-instrumentations/src/helpers/register.js +9 -64
  26. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +46 -14
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/azure-durable-functions.js +17 -0
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
  32. package/packages/datadog-instrumentations/src/langchain.js +1 -1
  33. package/packages/datadog-instrumentations/src/langgraph.js +1 -1
  34. package/packages/datadog-instrumentations/src/mercurius.js +1 -1
  35. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
  36. package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
  37. package/packages/datadog-instrumentations/src/openai.js +54 -76
  38. package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
  39. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
  40. package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
  41. package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
  42. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
  43. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
  44. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
  45. package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
  46. package/packages/datadog-turbopack/index.js +212 -0
  47. package/packages/datadog-turbopack/src/constants.js +5 -0
  48. package/packages/datadog-turbopack/src/loader.js +500 -0
  49. package/packages/dd-trace/src/aiguard/index.js +2 -1
  50. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
  51. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
  52. package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
  53. package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
  54. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
  55. package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
  56. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
  57. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
  58. package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
  59. package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
  60. package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
  61. package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
  62. package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
  63. package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
  64. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  65. package/packages/dd-trace/src/config/helper.js +10 -2
  66. package/packages/dd-trace/src/config/index.js +20 -5
  67. package/packages/dd-trace/src/config/supported-configurations.json +8 -0
  68. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
  69. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
  70. package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
  71. package/packages/dd-trace/src/evp_proxy/path.js +12 -3
  72. package/packages/dd-trace/src/exporters/common/docker.js +4 -1
  73. package/packages/dd-trace/src/exporters/common/proxy.js +3 -0
  74. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  75. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
  76. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
  77. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +7 -1
  78. package/packages/dd-trace/src/plugin_manager.js +9 -3
  79. package/packages/dd-trace/src/plugins/index.js +1 -0
  80. package/packages/dd-trace/src/plugins/util/test.js +4 -1
  81. 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
- // Provider lifecycle channels. Payloads stay OpenAI-native:
12
- // before { args, parentSpan, abortController, pending }
13
- // after { args, body, parentSpan, abortController, pending }
14
- const chatCompletionsBeforeChannel = dc.channel('dd-trace:openai:chat.completions:before')
15
- const chatCompletionsAfterChannel = dc.channel('dd-trace:openai:chat.completions:after')
16
- const responsesBeforeChannel = dc.channel('dd-trace:openai:responses:before')
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
- * Publishes a provider-native lifecycle payload to a cancelable lifecycle channel.
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
- * @param {object} channel
37
- * @param {object} payload
38
- * @returns {Promise<void>}
22
+ * @template T
23
+ * @param {Promise<T>} promise
24
+ * @param {Promise<void>|undefined} settled
25
+ * @returns {Promise<T>}
39
26
  */
40
- function publishLifecycle (channel, payload) {
41
- const abortController = new AbortController()
42
- const ctx = { ...payload, abortController, pending: [] }
27
+ function heldUntil (promise, settled) {
28
+ if (!settled) return promise
43
29
 
44
- channel.publish(ctx)
45
-
46
- return Promise.all(ctx.pending).then(() => {
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 Promise.all([state.getBeforeVerdict(), responsePromise]).then(([, response]) => response)
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 channels = stream ? null : LIFECYCLE_CHANNELS[baseResource]
318
- const hasLifecycle = !!channels && (channels.before.hasSubscribers || channels.after.hasSubscribers)
300
+ const intercepted = !stream && interceptChannel?.hasSubscribers
319
301
 
320
- if (!ch.start.hasSubscribers && !hasLifecycle) {
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
- const beforeChannel = hasLifecycle && channels.before.hasSubscribers ? channels.before : null
340
- const afterChannel = hasLifecycle && channels.after.hasSubscribers ? channels.after : null
341
- let beforeVerdict
342
- const getBeforeVerdict = beforeChannel
343
- ? function getBeforeVerdict () {
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
- const state = { ctx, stream, getBeforeVerdict, afterChannel, parentSpan }
350
- responsePromiseContexts.set(apiProm.responsePromise, state)
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, getBeforeVerdict, afterChannel, parentSpan } = state
365
- const gatedApiProm = getBeforeVerdict
366
- ? Promise.all([getBeforeVerdict(), apiProm]).then(([, result]) => result)
367
- : apiProm
343
+ const { ctx, stream, interceptCtx } = state
368
344
 
369
- return gatedApiProm
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', wrapStreamIterator(response, options, ctx))
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 (!afterChannel) {
367
+ if (!interceptCtx?.onResult) {
392
368
  finish(ctx, responseData)
393
369
  return body
394
370
  }
395
371
 
396
- // Finish after evaluation so a block propagates the error to openai.request
397
- // and the span wraps its child instead of closing before it.
398
- return publishLifecycle(afterChannel, { args: ctx.args, body, parentSpan }).then(() => {
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 body
376
+ return deliveredBody
401
377
  })
402
378
  })
403
379
  .catch(error => {
404
- // ctx.result is set inside finish(); if absent, finish never ran (sync throw in the success
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 shouldFlipStatus = isEfd || isAtf
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 (shouldFlipStatus) {
742
- // If we don't "reset" the result.state to "pass", once a repetition fails,
743
- // vitest will always consider the test as failed, so we can't read the actual status
744
- // This means that we change vitest's behavior:
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 (shouldFlipStatus) {
763
- task.result.state = 'pass'
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') prompt.push({ content: block.text, role: 'system' })
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
- span.finish()
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
- kind: 'internal',
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 = this.#reconstructPubSubRequestContext(message.attributes)
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 = this.#reconstructPubSubContext(messageData.attrs) || originalContext
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
- for (let field = rootCtx?.resolveFields; field; field = field.nextResolveField) {
67
- if ((!fieldNode || field.fieldNode === fieldNode) && matchesPath(field.pathString, path)) {
68
- recordResolveError(field, error)
69
- return
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 {boolean}
73
+ * @returns {string}
78
74
  */
79
- function matchesPath (fieldPath, path) {
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 === fieldPath
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
  /**