dd-trace 6.14.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 (98) 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 +12 -0
  6. package/next.d.ts +27 -0
  7. package/next.js +3 -0
  8. package/package.json +17 -13
  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/playwright.js +1 -5
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +6 -7
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +99 -70
  33. package/packages/datadog-instrumentations/src/jest.js +141 -25
  34. package/packages/datadog-instrumentations/src/langchain.js +1 -1
  35. package/packages/datadog-instrumentations/src/langgraph.js +1 -1
  36. package/packages/datadog-instrumentations/src/mercurius.js +1 -1
  37. package/packages/datadog-instrumentations/src/mocha/main.js +41 -5
  38. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +1 -1
  39. package/packages/datadog-instrumentations/src/openai-agents.js +1 -1
  40. package/packages/datadog-instrumentations/src/openai.js +54 -76
  41. package/packages/datadog-instrumentations/src/playwright.js +4 -0
  42. package/packages/datadog-instrumentations/src/vitest-worker.js +7 -10
  43. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +7 -1
  44. package/packages/datadog-plugin-azure-cosmos/src/index.js +1 -1
  45. package/packages/datadog-plugin-azure-durable-functions/src/index.js +96 -1
  46. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -24
  47. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +2 -36
  48. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-request-context.js +31 -0
  49. package/packages/datadog-plugin-graphql/src/resolve-error.js +33 -11
  50. package/packages/datadog-turbopack/index.js +212 -0
  51. package/packages/datadog-turbopack/src/constants.js +5 -0
  52. package/packages/datadog-turbopack/src/loader.js +500 -0
  53. package/packages/dd-trace/src/aiguard/index.js +2 -1
  54. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +53 -25
  55. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +49 -25
  56. package/packages/dd-trace/src/aiguard/integrations/index.js +3 -2
  57. package/packages/dd-trace/src/aiguard/integrations/openai.js +59 -30
  58. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +116 -42
  59. package/packages/dd-trace/src/aiguard/messages/utils.js +29 -1
  60. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +25 -0
  61. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +3 -1
  62. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +531 -70
  63. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +2 -2
  64. package/packages/dd-trace/src/appsec/telemetry/api_security.js +1 -3
  65. package/packages/dd-trace/src/appsec/telemetry/common.js +4 -0
  66. package/packages/dd-trace/src/appsec/telemetry/index.js +23 -5
  67. package/packages/dd-trace/src/appsec/telemetry/rasp.js +1 -20
  68. package/packages/dd-trace/src/appsec/telemetry/user.js +1 -3
  69. package/packages/dd-trace/src/appsec/telemetry/waf.js +1 -20
  70. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -0
  71. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +4 -0
  72. package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
  73. package/packages/dd-trace/src/config/helper.js +10 -2
  74. package/packages/dd-trace/src/config/index.js +20 -5
  75. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  76. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -7
  77. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +14 -1
  78. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +1 -6
  79. package/packages/dd-trace/src/evp_proxy/direct.js +2 -28
  80. package/packages/dd-trace/src/evp_proxy/discovery.js +3 -4
  81. package/packages/dd-trace/src/evp_proxy/path.js +12 -3
  82. package/packages/dd-trace/src/exporters/agentless/writer.js +3 -1
  83. package/packages/dd-trace/src/exporters/common/docker.js +4 -1
  84. package/packages/dd-trace/src/exporters/common/proxy.js +55 -0
  85. package/packages/dd-trace/src/exporters/common/request.js +11 -3
  86. package/packages/dd-trace/src/opentelemetry/metrics/index.js +1 -1
  87. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +5 -4
  88. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +106 -25
  89. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +12 -1
  90. package/packages/dd-trace/src/opentracing/propagation/text_map.js +8 -2
  91. package/packages/dd-trace/src/plugin_manager.js +9 -3
  92. package/packages/dd-trace/src/plugins/index.js +1 -0
  93. package/packages/dd-trace/src/plugins/util/test.js +4 -1
  94. package/packages/dd-trace/src/priority_sampler.js +4 -0
  95. package/packages/dd-trace/src/sampling_rule.js +3 -1
  96. package/packages/dd-trace/src/span_processor.js +18 -3
  97. package/packages/dd-trace/src/telemetry/send-data.js +5 -4
  98. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +0 -10
@@ -6,90 +6,30 @@ const { addHook } = require('./helpers/instrument')
6
6
 
7
7
  const anthropicTracingChannel = tracingChannel('apm:anthropic:request')
8
8
  const onStreamedChunkCh = channel('apm:anthropic:request:chunk')
9
- const messagesBeforeChannel = channel('dd-trace:anthropic:messages:before')
10
- const messagesAfterChannel = channel('dd-trace:anthropic:messages:after')
11
9
 
12
- /**
13
- * Publishes a provider-native lifecycle payload to a cancelable lifecycle channel.
14
- *
15
- * Subscribers push async work into `pending` synchronously during publication and
16
- * abort `abortController` with an error before the pushed promise resolves to block.
17
- *
18
- * @param {object} channel
19
- * @param {object} payload
20
- * @returns {Promise<void>}
21
- */
22
- function publishLifecycle (channel, payload) {
23
- const abortController = new AbortController()
24
- const ctx = { ...payload, abortController, pending: [] }
25
-
26
- channel.publish(ctx)
27
-
28
- return Promise.all(ctx.pending).then(() => {
29
- if (abortController.signal.aborted) {
30
- throw abortController.signal.reason
31
- }
32
- })
33
- }
10
+ // `prepare` is published before the SDK is invoked, so subscribers may replace `arguments[0]`.
11
+ // `intercept` follows with the native call data; a subscriber may set either callback and this
12
+ // 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 messagesPrepareChannel = channel('dd-trace:anthropic:messages:prepare')
16
+ const messagesInterceptChannel = channel('dd-trace:anthropic:messages:intercept')
34
17
 
35
18
  /**
19
+ * Returns `promise`, but not before `settled` settles, so a rejection from an interceptor
20
+ * reaches the caller instead of the SDK's value.
21
+ *
36
22
  * @template T
37
23
  * @param {Promise<T>} promise
38
- * @param {Promise<void>|undefined} verdict
24
+ * @param {Promise<void>|undefined} settled
39
25
  * @returns {Promise<T>}
40
26
  */
41
- function waitForVerdict (promise, verdict) {
42
- if (!verdict) return promise
27
+ function heldUntil (promise, settled) {
28
+ if (!settled) return promise
43
29
 
44
- // The lifecycle verdict takes precedence over an earlier SDK rejection.
30
+ // The interceptor's rejection wins over an earlier SDK rejection, so keep that one handled.
45
31
  promise.catch(() => {})
46
- return verdict.then(() => promise)
47
- }
48
-
49
- /**
50
- * @param {Array<unknown>} args
51
- * @returns {Array<unknown>|undefined}
52
- */
53
- function snapshotLifecycleArgs (args) {
54
- const options = args[0]
55
- if (!options || typeof options !== 'object') return
56
-
57
- const input = { messages: options.messages }
58
- if (options.system !== undefined) input.system = options.system
59
-
60
- // Snapshot via JSON so AI Guard evaluates exactly what the SDK serializes and sends, immune to
61
- // later caller mutation. If it can't serialize (e.g. circular), the SDK's own serialization would
62
- // fail too, so skipping evaluation is safe — nothing reaches the model.
63
- try {
64
- const snapshot = [...args]
65
- // eslint-disable-next-line unicorn/prefer-structured-clone
66
- snapshot[0] = { ...options, ...JSON.parse(JSON.stringify(input)) }
67
- return snapshot
68
- } catch {
69
- // Unserializable input — leave AI Guard inactive for this call (the SDK send would fail too).
70
- }
71
- }
72
-
73
- /**
74
- * Finishes after output evaluation so a block propagates to the caller and span.
75
- *
76
- * @param {object} ctx
77
- * @param {object} result
78
- * @param {(body: object) => Promise<void>|undefined} getVerdict
79
- * @param {object|string} [returnedResult]
80
- * @returns {object|string|Promise<object|string>}
81
- */
82
- function finishResult (ctx, result, getVerdict, returnedResult = result) {
83
- const verdict = getVerdict(result)
84
- if (!verdict) {
85
- finish(ctx, result)
86
- return returnedResult
87
- }
88
-
89
- return verdict.then(() => {
90
- finish(ctx, result)
91
- return returnedResult
92
- })
32
+ return settled.then(() => promise)
93
33
  }
94
34
 
95
35
  /**
@@ -106,22 +46,36 @@ function finishAndThrow (ctx, error) {
106
46
  * @param {object} response
107
47
  * @param {'json'|'text'} method
108
48
  * @param {object} ctx
109
- * @param {(body: object) => Promise<void>|undefined} getVerdict
49
+ * @param {object} [interceptCtx]
110
50
  */
111
- function wrapResponseReader (response, method, ctx, getVerdict) {
51
+ function wrapResponseReader (response, method, ctx, interceptCtx) {
112
52
  if (typeof response[method] !== 'function') return
113
53
 
114
54
  shimmer.wrap(response, method, original => function (...args) {
115
55
  return original.apply(this, args)
116
56
  .then(body => {
117
- if (method === 'json') return finishResult(ctx, body, getVerdict)
57
+ // `text()` callers keep their string; only the callback and the span see the parsed body.
58
+ let parsed = body
59
+ if (method === 'text') {
60
+ try {
61
+ parsed = JSON.parse(body)
62
+ } catch {
63
+ finish(ctx)
64
+ return body
65
+ }
66
+ }
118
67
 
119
- try {
120
- return finishResult(ctx, JSON.parse(body), getVerdict, body)
121
- } catch {
122
- finish(ctx)
68
+ if (!interceptCtx?.onResult) {
69
+ finish(ctx, parsed)
123
70
  return body
124
71
  }
72
+
73
+ return Promise.resolve(interceptCtx.onResult(parsed)).then(deliveredBody => {
74
+ finish(ctx, parsed)
75
+ // `json()` callers get any replacement; `text()` callers keep their original string,
76
+ // because the callback was handed the parsed object rather than the text.
77
+ return method === 'json' ? deliveredBody : body
78
+ })
125
79
  })
126
80
  .catch(error => finishAndThrow(ctx, error))
127
81
  })
@@ -130,17 +84,17 @@ function wrapResponseReader (response, method, ctx, getVerdict) {
130
84
  /**
131
85
  * @param {object} response
132
86
  * @param {object} ctx
133
- * @param {(body: object) => Promise<void>|undefined} getVerdict
87
+ * @param {object} [interceptCtx]
134
88
  */
135
- function wrapRawResponse (response, ctx, getVerdict) {
136
- wrapResponseReader(response, 'json', ctx, getVerdict)
137
- wrapResponseReader(response, 'text', ctx, getVerdict)
89
+ function wrapRawResponse (response, ctx, interceptCtx) {
90
+ wrapResponseReader(response, 'json', ctx, interceptCtx)
91
+ wrapResponseReader(response, 'text', ctx, interceptCtx)
138
92
 
139
93
  if (typeof response.clone !== 'function') return
140
94
 
141
95
  shimmer.wrap(response, 'clone', clone => function (...args) {
142
96
  const clonedResponse = clone.apply(this, args)
143
- wrapRawResponse(clonedResponse, ctx, getVerdict)
97
+ wrapRawResponse(clonedResponse, ctx, interceptCtx)
144
98
  return clonedResponse
145
99
  })
146
100
  }
@@ -172,68 +126,60 @@ function wrapStreamIterator (iterator, ctx) {
172
126
 
173
127
  function wrapCreate (create) {
174
128
  return function (...args) {
175
- const options = args[0]
176
- const stream = options?.stream
177
-
178
- // Streaming is out of scope for lifecycle evaluation.
179
- const lifecycleArgs = !stream &&
180
- (messagesBeforeChannel.hasSubscribers || messagesAfterChannel.hasSubscribers)
181
- ? snapshotLifecycleArgs(args)
182
- : undefined
129
+ const stream = args[0]?.stream
130
+ const preparing = !stream && messagesPrepareChannel.hasSubscribers
131
+ const intercepting = !stream && messagesInterceptChannel.hasSubscribers
183
132
 
184
- if (!anthropicTracingChannel.start.hasSubscribers && !lifecycleArgs) {
133
+ if (!anthropicTracingChannel.start.hasSubscribers && !preparing && !intercepting) {
185
134
  return create.apply(this, args)
186
135
  }
187
136
 
188
- const ctx = { options: lifecycleArgs?.[0] ?? options, resource: 'create', baseUrl: this._client?.baseURL }
137
+ // `args` is this wrapper's own rest array, so replacing `arguments[0]` changes what reaches
138
+ // the SDK without touching anything the caller still holds.
139
+ if (preparing) {
140
+ messagesPrepareChannel.publish({ arguments: args })
141
+ }
189
142
 
190
- return anthropicTracingChannel.start.runStores(ctx, () => {
191
- const parentSpan = lifecycleArgs ? ctx.currentStore?.span : undefined
143
+ const ctx = { options: args[0], resource: 'create', baseUrl: this._client?.baseURL }
192
144
 
145
+ return anthropicTracingChannel.start.runStores(ctx, () => {
193
146
  let apiPromise
194
147
  try {
195
- // Anthropic starts the request eagerly; the input verdict only gates result delivery.
196
- apiPromise = create.apply(this, lifecycleArgs ?? args)
148
+ // Anthropic starts the request eagerly, so subscribers can only hold back delivery.
149
+ apiPromise = create.apply(this, args)
197
150
  } catch (error) {
198
151
  finish(ctx, null, error)
199
152
  throw error
200
153
  }
201
154
 
202
- let beforeVerdict
203
- let afterVerdict
204
- let parseResult
205
- let wrappedResponse
206
-
207
- function getBeforeVerdict () {
208
- if (!lifecycleArgs || beforeVerdict) return beforeVerdict
209
- if (!messagesBeforeChannel.hasSubscribers) return
210
-
211
- beforeVerdict = publishLifecycle(messagesBeforeChannel, { args: lifecycleArgs, parentSpan })
212
- return beforeVerdict
155
+ let interceptCtx
156
+ if (intercepting) {
157
+ interceptCtx = { arguments: args, tracingContext: ctx }
158
+ messagesInterceptChannel.publish(interceptCtx)
213
159
  }
214
160
 
215
- /**
216
- * @param {object|string} body
217
- */
218
- function getAfterVerdict (body) {
219
- if (!lifecycleArgs || afterVerdict) return afterVerdict
220
- if (!messagesAfterChannel.hasSubscribers) return
221
-
222
- afterVerdict = publishLifecycle(messagesAfterChannel, { args: lifecycleArgs, body, parentSpan })
223
- return afterVerdict
224
- }
161
+ let parseResult
162
+ let wrappedResponse
225
163
 
226
164
  shimmer.wrap(apiPromise, 'parse', parse => function (...parseArgs) {
227
165
  if (parseResult) return parseResult
228
166
 
229
- const parsed = parse.apply(this, parseArgs)
230
- parseResult = waitForVerdict(parsed, getBeforeVerdict())
167
+ parseResult = heldUntil(parse.apply(this, parseArgs), interceptCtx?.beforeResult?.())
231
168
  .then(response => {
232
169
  if (stream) {
233
170
  shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx))
234
171
  return response
235
172
  }
236
- return finishResult(ctx, response, getAfterVerdict)
173
+
174
+ if (!interceptCtx?.onResult) {
175
+ finish(ctx, response)
176
+ return response
177
+ }
178
+
179
+ return Promise.resolve(interceptCtx.onResult(response)).then(deliveredResponse => {
180
+ finish(ctx, response)
181
+ return deliveredResponse
182
+ })
237
183
  })
238
184
  .catch(error => finishAndThrow(ctx, error))
239
185
 
@@ -242,18 +188,14 @@ function wrapCreate (create) {
242
188
 
243
189
  if (typeof apiPromise.asResponse === 'function') {
244
190
  shimmer.wrap(apiPromise, 'asResponse', origAsResponse => function (...asResponseArgs) {
245
- return waitForVerdict(origAsResponse.apply(this, asResponseArgs), getBeforeVerdict())
191
+ return heldUntil(origAsResponse.apply(this, asResponseArgs), interceptCtx?.beforeResult?.())
246
192
  .then(response => {
247
- // Wrap the raw response's json()/text()/clone() readers to evaluate output on read,
248
- // but only while someone still consumes the result and not twice for the same response.
249
- const outputObserved = anthropicTracingChannel.start.hasSubscribers ||
250
- afterVerdict || messagesAfterChannel.hasSubscribers
251
- if (!stream && outputObserved && wrappedResponse !== response) {
193
+ // Wrap json()/text()/clone() so the span still closes on the raw-response path,
194
+ // and not twice for the same response.
195
+ if (!stream && wrappedResponse !== response) {
252
196
  wrappedResponse = response
253
- wrapRawResponse(response, ctx, getAfterVerdict)
197
+ wrapRawResponse(response, ctx, interceptCtx)
254
198
  }
255
-
256
- if (afterVerdict) return afterVerdict.then(() => response)
257
199
  return response
258
200
  })
259
201
  .catch(error => finishAndThrow(ctx, error))
@@ -105,7 +105,7 @@ function observeDurablePromise (dp, onSettle) {
105
105
  dp[ON_SETTLE] = onSettle
106
106
  }
107
107
 
108
- for (const hook of getHooks('@aws/durable-execution-sdk-js')) {
108
+ for (const hook of getHooks('@aws/durable-execution-sdk-js').values()) {
109
109
  hook.file = null
110
110
  addHook(hook, exports => exports)
111
111
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  const { addHook, getHooks } = require('./helpers/instrument')
4
4
 
5
- for (const hook of getHooks('@azure/cosmos')) {
5
+ for (const hook of getHooks('@azure/cosmos').values()) {
6
6
  addHook(hook, exports => exports)
7
7
  }
@@ -5,6 +5,7 @@ const shimmer = require('../../datadog-shimmer')
5
5
 
6
6
  const {
7
7
  addHook,
8
+ getHooks,
8
9
  } = require('./helpers/instrument')
9
10
 
10
11
  /**
@@ -12,6 +13,10 @@ const {
12
13
  */
13
14
  const azureDurableFunctionsChannel = dc.tracingChannel('datadog:azure:durable-functions:invoke')
14
15
 
16
+ for (const hook of getHooks('durable-functions').values()) {
17
+ addHook(hook, exports => exports)
18
+ }
19
+
15
20
  addHook({ name: 'durable-functions', versions: ['>=3'], patchDefault: false }, (df) => {
16
21
  const { app } = df
17
22
 
@@ -42,9 +47,16 @@ function entityHandler (handler, entityName) {
42
47
  if (!azureDurableFunctionsChannel.hasSubscribers) return handler.apply(this, args)
43
48
 
44
49
  const entityContext = args[0]
50
+ const traceContext = entityContext?.traceContext
45
51
  return azureDurableFunctionsChannel.traceSync(
46
52
  handler,
47
- { trigger: 'Entity', functionName: entityName, operationName: entityContext?.df?.operationName },
53
+ {
54
+ trigger: 'Entity',
55
+ functionName: entityName,
56
+ operationName: entityContext?.df?.operationName,
57
+ traceparent: traceContext?.traceParent,
58
+ tracestate: traceContext?.traceState,
59
+ },
48
60
  this, ...args)
49
61
  }
50
62
  }
@@ -58,16 +70,18 @@ function activityHandler (method) {
58
70
  return function (...args) {
59
71
  if (!azureDurableFunctionsChannel.hasSubscribers) return handler.apply(this, args)
60
72
 
73
+ const traceContext = args[1]?.traceContext
74
+ const channelCtx = {
75
+ trigger: 'Activity',
76
+ functionName: activityName,
77
+ traceparent: traceContext?.traceParent,
78
+ tracestate: traceContext?.traceState,
79
+ }
80
+
61
81
  // use tracePromise if this is an async handler. otherwise, use traceSync
62
82
  return isAsync
63
- ? azureDurableFunctionsChannel.tracePromise(
64
- handler,
65
- { trigger: 'Activity', functionName: activityName },
66
- this, ...args)
67
- : azureDurableFunctionsChannel.traceSync(
68
- handler,
69
- { trigger: 'Activity', functionName: activityName },
70
- this, ...args)
83
+ ? azureDurableFunctionsChannel.tracePromise(handler, channelCtx, this, ...args)
84
+ : azureDurableFunctionsChannel.traceSync(handler, channelCtx, this, ...args)
71
85
  }
72
86
  })
73
87
  return method.apply(this, arguments)
@@ -8,6 +8,12 @@ const {
8
8
 
9
9
  const azureFunctionsChannel = dc.tracingChannel('datadog:azure:functions:invoke')
10
10
 
11
+ // Orchestrators register via app.generic; activities/entities use the durable-functions hook.
12
+ const azureDurableFunctionsChannel = dc.tracingChannel('datadog:azure:durable-functions:invoke')
13
+
14
+ const ORCHESTRATION_TRIGGER_TYPE = 'orchestrationTrigger'
15
+ const ORCHESTRATOR_COMPLETED_EVENT_TYPE = 13
16
+
11
17
  addHook({ name: '@azure/functions', versions: ['>=4'], patchDefault: false }, (azureFunction) => {
12
18
  const { app } = azureFunction
13
19
 
@@ -29,9 +35,51 @@ addHook({ name: '@azure/functions', versions: ['>=4'], patchDefault: false }, (a
29
35
  // CosmosDB triggers
30
36
  shimmer.wrap(app, 'cosmosDB', wrapHandler)
31
37
 
38
+ // Durable Functions orchestration triggers
39
+ shimmer.wrap(app, 'generic', wrapGeneric)
40
+
32
41
  return azureFunction
33
42
  })
34
43
 
44
+ function wrapGeneric (method) {
45
+ return function (name, options) {
46
+ if (options?.trigger?.type === ORCHESTRATION_TRIGGER_TYPE && typeof options.handler === 'function') {
47
+ options.handler = traceOrchestrationHandler(options.handler, name)
48
+ }
49
+ return method.apply(this, arguments)
50
+ }
51
+ }
52
+
53
+ function traceOrchestrationHandler (handler, functionName) {
54
+ return function (...args) {
55
+ if (!azureDurableFunctionsChannel.hasSubscribers) return handler.apply(this, args)
56
+
57
+ const orchestrationBinding = args[0]
58
+ // we do not want to trace if the orchestrator has already completed and is being reactivated
59
+ const history = orchestrationBinding?.history
60
+ const hasPreviousActivation = history?.some(
61
+ event => event.EventType === ORCHESTRATOR_COMPLETED_EVENT_TYPE
62
+ )
63
+
64
+ if (hasPreviousActivation) {
65
+ return handler.apply(this, args)
66
+ }
67
+
68
+ const traceContext = args[1]?.traceContext
69
+ const channelCtx = {
70
+ trigger: 'Orchestration',
71
+ functionName,
72
+ traceparent: traceContext?.traceParent,
73
+ tracestate: traceContext?.traceState,
74
+ }
75
+
76
+ return azureDurableFunctionsChannel.tracePromise(
77
+ handler,
78
+ channelCtx,
79
+ this, ...args)
80
+ }
81
+ }
82
+
35
83
  // The http methods are overloaded so we need to check which type of argument was passed in order to wrap the handler
36
84
  // The arguments are either an object with a handler property or the handler function itself
37
85
  function wrapHandler (method) {
@@ -6,6 +6,6 @@
6
6
 
7
7
  const { addHook, getHooks } = require('./helpers/instrument')
8
8
 
9
- for (const hook of getHooks('bullmq')) {
9
+ for (const hook of getHooks('bullmq').values()) {
10
10
  addHook(hook, exports => exports)
11
11
  }
@@ -638,7 +638,7 @@ function wrapQueryAsyncIterator (asyncIterator, ctx) {
638
638
 
639
639
  let querySubscribed = false
640
640
 
641
- for (const hook of getHooks('@anthropic-ai/claude-agent-sdk')) {
641
+ for (const hook of getHooks('@anthropic-ai/claude-agent-sdk').values()) {
642
642
  hook.file = null
643
643
 
644
644
  addHook(hook, exports => {
@@ -3,5 +3,5 @@
3
3
  const { addHook } = require('./helpers/instrument')
4
4
 
5
5
  // Empty hook just to make the plugin load.
6
- // TODO: Add version range when the module is released on npm.
7
- addHook({ name: 'dd-trace-api' }, api => api)
6
+ // Version 1.0.1 re-publishes tracer initialization after the plugin subscribes.
7
+ addHook({ name: 'dd-trace-api', versions: ['>=1.0.1'] }, api => api)
@@ -12,7 +12,7 @@ const { addHook, getHooks } = require('./helpers/instrument')
12
12
  */
13
13
  function addRewriterHooks (name) {
14
14
  const files = new Set()
15
- for (const hook of getHooks(name)) {
15
+ for (const hook of getHooks(name).values()) {
16
16
  if (files.has(hook.file)) continue
17
17
  files.add(hook.file)
18
18
  addHook(hook, exports => exports)
@@ -4,13 +4,16 @@ const Module = require('module')
4
4
  const dc = require('dc-polyfill')
5
5
 
6
6
  const log = require('../../../dd-trace/src/log')
7
+ const { loadChannel } = require('./register.js')
7
8
  const {
8
9
  filename,
9
- loadChannel,
10
+ getDisabledInstrumentations,
10
11
  matchVersion,
11
- } = require('./register.js')
12
+ matchesInstrumentation,
13
+ } = require('./instrumentation-utils')
12
14
  const hooks = require('./hooks')
13
15
  const instrumentations = require('./instrumentations')
16
+ const disabledInstrumentations = getDisabledInstrumentations()
14
17
 
15
18
  // register.js has now set up ritm (require-in-the-middle). In bundled
16
19
  // environments (webpack, esbuild), Node.js built-in modules required by
@@ -69,21 +72,29 @@ function doHook (name) {
69
72
 
70
73
  try {
71
74
  hookFn()
72
- } catch {
73
- log.error('esbuild-wrapped %s hook failed', name)
75
+ } catch (error) {
76
+ log.error('esbuild-wrapped %s hook failed: %s', name, String(error?.message ?? error), error)
74
77
  }
75
78
  }
76
79
 
80
+ /**
81
+ * @typedef {object} Payload
82
+ * @property {boolean} [activate]
83
+ * @property {string} package
84
+ * @property {unknown} [module]
85
+ * @property {string} [version]
86
+ * @property {string} [path]
87
+ */
88
+
77
89
  /** @type {Set<string>} */
78
90
  const instrumentedNodeModules = new Set()
79
91
 
80
- /** @typedef {{ package: string, module: unknown, version: string, path: string }} Payload */
81
92
  dc.subscribe(CHANNEL, (message) => {
82
93
  const payload = /** @type {Payload} */ (message)
83
94
  const name = payload.package
95
+ if (disabledInstrumentations.has(name)) return
84
96
 
85
97
  const isPrefixedWithNode = name.startsWith('node:')
86
-
87
98
  const isNodeModule = isPrefixedWithNode || !hooks[name]
88
99
 
89
100
  if (isNodeModule) {
@@ -100,20 +111,34 @@ dc.subscribe(CHANNEL, (message) => {
100
111
  const instrumentation = instrumentations[name] ?? instrumentations[`node:${name}`]
101
112
 
102
113
  if (!instrumentation) {
103
- log.error('esbuild-wrapped %s missing in list of instrumentations', name)
114
+ if (!payload.activate) log.error('esbuild-wrapped %s missing in list of instrumentations', name)
104
115
  return
105
116
  }
106
117
 
107
- for (const { file, versions, hook } of instrumentation) {
108
- if (payload.path !== filename(name, file) || !matchVersion(payload.version, versions)) {
109
- continue
118
+ if (payload.activate) {
119
+ for (const { sourceRewrite, versions, hook } of instrumentation) {
120
+ if (!sourceRewrite || payload.path !== filename(name, sourceRewrite) ||
121
+ !matchVersion(payload.version, versions)) continue
122
+
123
+ try {
124
+ loadChannel.publish({ name })
125
+ hook(undefined, payload.version)
126
+ } catch (error) {
127
+ log.error('Error executing bundler hook: %s', String(error?.message ?? error), error)
128
+ }
129
+ return
110
130
  }
131
+ return
132
+ }
133
+
134
+ for (const entry of instrumentation) {
135
+ if (!matchesInstrumentation(name, payload.version, payload.path, entry)) continue
111
136
 
112
137
  try {
113
- loadChannel.publish({ name, version: payload.version, file })
114
- payload.module = hook(payload.module, payload.version) ?? payload.module
115
- } catch (e) {
116
- log.error('Error executing bundler hook', e)
138
+ loadChannel.publish({ name })
139
+ payload.module = entry.hook(payload.module, payload.version) ?? payload.module
140
+ } catch (error) {
141
+ log.error('Error executing bundler hook: %s', String(error?.message ?? error), error)
117
142
  }
118
143
  }
119
144
  })
@@ -5,6 +5,8 @@ const dc = /** @type {typeof import('node:diagnostics_channel')} */ (require('dc
5
5
  const instrumentations = require('./instrumentations')
6
6
  const rewriterInstrumentations = require('./rewriter/instrumentations')
7
7
 
8
+ const sourceRewritePaths = new WeakMap()
9
+
8
10
  /**
9
11
  * @typedef {import('node:diagnostics_channel').Channel} Channel
10
12
  * @typedef {import('node:diagnostics_channel').TracingChannel} TracingChannel
@@ -74,13 +76,26 @@ exports.createErrorPublisher = function createErrorPublisher (errorChannel) {
74
76
  }
75
77
  }
76
78
 
79
+ // The rewriter instrumentation list holds one entry per *transform*, so a
80
+ // module with several transforms repeats its module definition several times
81
+ // (mercurius defines 3, graphql 26). A hook only cares about the module, not
82
+ // the transform, so duplicates would push the same (versionRange, filePath)
83
+ // registration through `addHook` once per transform and have shimmer patch the
84
+ // same file several times.
77
85
  exports.getHooks = function getHooks (names) {
78
- names = [names].flat()
79
-
80
- return rewriterInstrumentations
81
- .map(inst => inst.module)
82
- .filter(({ name }) => names.includes(name))
83
- .map(({ name, versionRange, filePath }) => ({ name, versions: [versionRange], file: filePath }))
86
+ const requested = new Set([names].flat())
87
+ const hooks = new Map()
88
+ for (const { module } of rewriterInstrumentations) {
89
+ if (!requested.has(module.name)) continue
90
+ // Fresh objects, including the versions array: callers may adjust a hook
91
+ // for their own registration (the ai, claude-agent-sdk and
92
+ // aws-durable-execution-sdk-js plugins set `hook.file = null`), which must
93
+ // not leak into any other call.
94
+ const hook = { name: module.name, versions: [module.versionRange], file: module.filePath }
95
+ sourceRewritePaths.set(hook, module.filePath)
96
+ hooks.set(`${module.name}|${module.versionRange}|${module.filePath}`, hook)
97
+ }
98
+ return hooks
84
99
  }
85
100
 
86
101
  /**
@@ -93,12 +108,14 @@ exports.getHooks = function getHooks (names) {
93
108
  * @param {(moduleExports: unknown, version: string, isIitm?: boolean, hookMeta?: object) => unknown} [hook]
94
109
  * Patches module exports
95
110
  */
96
- exports.addHook = function addHook ({ name, versions, file, filePattern, patchDefault }, hook) {
111
+ exports.addHook = function addHook (args, hook) {
112
+ const { name, versions, file, filePattern, patchDefault } = args
97
113
  if (!instrumentations[name]) {
98
114
  instrumentations[name] = []
99
115
  }
100
116
 
101
- instrumentations[name].push({ versions, file, filePattern, hook, patchDefault })
117
+ const sourceRewrite = sourceRewritePaths.get(args)
118
+ instrumentations[name].push({ versions, file, filePattern, hook, patchDefault, sourceRewrite })
102
119
  }
103
120
 
104
121
  exports.AsyncResource = AsyncResource