dd-trace 5.108.0 → 5.110.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 (174) hide show
  1. package/ci/init.js +4 -3
  2. package/index.d.ts +88 -25
  3. package/loader-hook.mjs +88 -2
  4. package/package.json +19 -14
  5. package/packages/datadog-esbuild/index.js +26 -0
  6. package/packages/datadog-esbuild/src/utils.js +46 -3
  7. package/packages/datadog-instrumentations/src/ai.js +43 -48
  8. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  9. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  11. package/packages/datadog-instrumentations/src/connect.js +4 -3
  12. package/packages/datadog-instrumentations/src/electron.js +1 -1
  13. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  14. package/packages/datadog-instrumentations/src/graphql.js +17 -6
  15. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  16. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  17. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  18. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  19. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  20. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  24. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  25. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  26. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  27. package/packages/datadog-instrumentations/src/koa.js +3 -2
  28. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  29. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  30. package/packages/datadog-instrumentations/src/next.js +44 -10
  31. package/packages/datadog-instrumentations/src/openai.js +77 -18
  32. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  33. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  34. package/packages/datadog-instrumentations/src/redis.js +0 -1
  35. package/packages/datadog-instrumentations/src/restify.js +4 -3
  36. package/packages/datadog-instrumentations/src/router.js +18 -5
  37. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  38. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  39. package/packages/datadog-instrumentations/src/vitest.js +323 -7
  40. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  44. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  45. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  46. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  47. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  48. package/packages/datadog-plugin-aws-sdk/src/base.js +14 -16
  49. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  50. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  51. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +108 -99
  52. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  53. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  54. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
  55. package/packages/datadog-plugin-fs/src/index.js +1 -0
  56. package/packages/datadog-plugin-http/src/client.js +8 -3
  57. package/packages/datadog-plugin-http2/src/client.js +5 -2
  58. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  59. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  60. package/packages/datadog-plugin-redis/src/index.js +1 -2
  61. package/packages/datadog-plugin-undici/src/index.js +5 -2
  62. package/packages/datadog-plugin-vitest/src/index.js +11 -2
  63. package/packages/datadog-webpack/index.js +17 -1
  64. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  65. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  66. package/packages/dd-trace/src/aiguard/index.js +11 -49
  67. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  68. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  69. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  70. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  71. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  72. package/packages/dd-trace/src/aiguard/sdk.js +13 -1
  73. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  74. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  75. package/packages/dd-trace/src/appsec/channels.js +1 -0
  76. package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
  77. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  78. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  79. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  80. package/packages/dd-trace/src/appsec/index.js +10 -6
  81. package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
  82. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  83. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  84. package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
  85. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  86. package/packages/dd-trace/src/config/defaults.js +22 -3
  87. package/packages/dd-trace/src/config/generated-config-types.d.ts +538 -1
  88. package/packages/dd-trace/src/config/helper.js +133 -33
  89. package/packages/dd-trace/src/config/index.js +4 -1
  90. package/packages/dd-trace/src/config/parsers.js +19 -1
  91. package/packages/dd-trace/src/config/supported-configurations.json +45 -5
  92. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +49 -35
  93. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  94. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  95. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  96. package/packages/dd-trace/src/debugger/index.js +5 -0
  97. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  98. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  99. package/packages/dd-trace/src/dogstatsd.js +15 -8
  100. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  101. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  102. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  103. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  104. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  105. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  106. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  107. package/packages/dd-trace/src/guardrails/index.js +8 -5
  108. package/packages/dd-trace/src/index.js +6 -4
  109. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  110. package/packages/dd-trace/src/llmobs/index.js +16 -5
  111. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  112. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  113. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  114. package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
  115. package/packages/dd-trace/src/llmobs/tagger.js +55 -0
  116. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  117. package/packages/dd-trace/src/log/index.js +18 -20
  118. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -1
  119. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  120. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  121. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  122. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  123. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  124. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  125. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  126. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  127. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  128. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
  129. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  130. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  131. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  132. package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
  133. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  134. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  135. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  136. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  137. package/packages/dd-trace/src/plugin_manager.js +7 -1
  138. package/packages/dd-trace/src/plugins/ci_plugin.js +36 -3
  139. package/packages/dd-trace/src/plugins/index.js +3 -0
  140. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  141. package/packages/dd-trace/src/plugins/util/test.js +80 -0
  142. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  143. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  144. package/packages/dd-trace/src/priority_sampler.js +1 -13
  145. package/packages/dd-trace/src/profiling/config.js +129 -198
  146. package/packages/dd-trace/src/profiling/constants.js +4 -7
  147. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  148. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  149. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  150. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  151. package/packages/dd-trace/src/profiling/oom.js +71 -0
  152. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  153. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  154. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  155. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  156. package/packages/dd-trace/src/ritm.js +4 -2
  157. package/packages/dd-trace/src/serverless.js +1 -2
  158. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  159. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  160. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  161. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  162. package/packages/dd-trace/src/span_format.js +2 -1
  163. package/packages/dd-trace/src/span_processor.js +6 -0
  164. package/packages/dd-trace/src/telemetry/send-data.js +12 -10
  165. package/packages/dd-trace/src/tracer.js +15 -1
  166. package/packages/dd-trace/src/util.js +14 -0
  167. package/register.js +58 -1
  168. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  169. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  170. package/vendor/dist/pprof-format/index.js +1 -1
  171. package/vendor/dist/protobufjs/index.js +1 -1
  172. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  173. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
  174. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -0,0 +1,114 @@
1
+ 'use strict'
2
+
3
+ const { storage } = require('../../datadog-core')
4
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
5
+ const { addOpMeta, unwrapDurableError } = require('./util')
6
+
7
+ // Span names whose direct children must keep the default resource.
8
+ // These can have very high cardinality which is undesireable in the resource.
9
+ const HIGH_CARDINALITY_PARENT_SPAN_NAMES = new Set([
10
+ 'aws.durable.map',
11
+ 'aws.durable.parallel',
12
+ ])
13
+
14
+ // The SDK emits these subTypes as internal scaffolding around map/parallel iterations
15
+ // and waitForCallback; not user-visible operations.
16
+ const SUPPRESSED_CHILD_CONTEXT_SUBTYPES = new Set([
17
+ 'Map',
18
+ 'Parallel',
19
+ 'MapIteration',
20
+ 'ParallelBranch',
21
+ 'WaitForCallback',
22
+ ])
23
+
24
+ class BaseContextPlugin extends TracingPlugin {
25
+ static id = 'aws-durable-execution-sdk-js'
26
+ static type = 'serverless'
27
+ static kind = 'internal'
28
+
29
+ constructor (...args) {
30
+ super(...args)
31
+ this.addSub(this.constructor.settleChannel, ctx => this.settle(ctx))
32
+ }
33
+
34
+ bindStart (ctx) {
35
+ const spanName = this.constructor.spanName
36
+ const parentName = this.activeSpan?.context()._name
37
+ const operationName = this.getOperationName(ctx)
38
+ const resource = HIGH_CARDINALITY_PARENT_SPAN_NAMES.has(parentName) ? undefined : operationName
39
+
40
+ const meta = {}
41
+ if (operationName) {
42
+ meta['aws.durable.operation_name'] = operationName
43
+ }
44
+ addOpMeta(meta, ctx.self)
45
+
46
+ this.startSpan(spanName, {
47
+ resource,
48
+ kind: this.constructor.kind,
49
+ meta,
50
+ }, ctx)
51
+
52
+ return ctx.currentStore
53
+ }
54
+
55
+ // All context methods have two overloads: method(name, …) and method(…); args[0] is the name in the first form.
56
+ getOperationName (ctx) {
57
+ const args = ctx.arguments || []
58
+ return typeof args[0] === 'string' ? args[0] : undefined
59
+ }
60
+
61
+ settle (ctx) {
62
+ if (ctx.suppressed) return
63
+ if (ctx.error !== undefined) {
64
+ ctx.currentStore?.span?.setTag('error', unwrapDurableError(ctx))
65
+ }
66
+ this.finish(ctx)
67
+ }
68
+
69
+ error (ctxOrError) {
70
+ this.settle(ctxOrError)
71
+ }
72
+ }
73
+
74
+ function makeContextPlugin (method, spanName) {
75
+ return class extends BaseContextPlugin {
76
+ static prefix = `tracing:orchestrion:@aws/durable-execution-sdk-js:DurableContextImpl_${method}`
77
+ static settleChannel = `apm:aws-durable-execution-sdk-js:${method}:settle`
78
+ static spanName = spanName
79
+ }
80
+ }
81
+
82
+ class RunInChildContextPlugin extends BaseContextPlugin {
83
+ static prefix = 'tracing:orchestrion:@aws/durable-execution-sdk-js:DurableContextImpl_runInChildContext'
84
+ static settleChannel = 'apm:aws-durable-execution-sdk-js:runInChildContext:settle'
85
+ static spanName = 'aws.durable.child_context'
86
+
87
+ bindStart (ctx) {
88
+ if (SUPPRESSED_CHILD_CONTEXT_SUBTYPES.has(getRunInChildContextSubType(ctx))) {
89
+ // Pass the active store through unchanged so any nested spans
90
+ // remain parented to the surrounding map/parallel span
91
+ ctx.suppressed = true
92
+ return storage('legacy').getStore()
93
+ }
94
+ return super.bindStart(ctx)
95
+ }
96
+ }
97
+
98
+ // runInChildContext has two overloads: `(name, fn, options)` and `(fn, options)`.
99
+ function getRunInChildContextSubType (ctx) {
100
+ const args = ctx.arguments || []
101
+ const opts = typeof args[0] === 'function' ? args[1] : args[2]
102
+ return opts?.subType
103
+ }
104
+
105
+ module.exports = {
106
+ step: makeContextPlugin('step', 'aws.durable.step'),
107
+ wait: makeContextPlugin('wait', 'aws.durable.wait'),
108
+ waitForCondition: makeContextPlugin('waitForCondition', 'aws.durable.wait_for_condition'),
109
+ waitForCallback: makeContextPlugin('waitForCallback', 'aws.durable.wait_for_callback'),
110
+ createCallback: makeContextPlugin('createCallback', 'aws.durable.create_callback'),
111
+ map: makeContextPlugin('map', 'aws.durable.map'),
112
+ parallel: makeContextPlugin('parallel', 'aws.durable.parallel'),
113
+ runInChildContext: RunInChildContextPlugin,
114
+ }
@@ -0,0 +1,128 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../dd-trace/src/log')
4
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
5
+ const { saveTraceContextCheckpointIfUpdated } = require('./trace-checkpoint')
6
+
7
+ // Termination reasons that indicate the execution is suspending rather than exiting permanently.
8
+ // Sourced from (`@aws/durable-execution-sdk-js`'s termination-manager/types.ts).
9
+ const PENDING_TERMINATION_REASONS = new Set([
10
+ 'OPERATION_TERMINATED',
11
+ 'RETRY_SCHEDULED',
12
+ 'RETRY_INTERRUPTED_STEP',
13
+ 'WAIT_SCHEDULED',
14
+ 'CALLBACK_PENDING',
15
+ 'CUSTOM',
16
+ ])
17
+
18
+ const DEFAULT_TERMINATION_REASON = 'OPERATION_TERMINATED'
19
+
20
+ // Published by the instrumentation when the SDK's terminationManager.terminate() is called.
21
+ // The instrumentation owns the wrapping; this plugin only reacts.
22
+ const TERMINATE_CHANNEL = 'apm:aws-durable-execution-sdk-js:terminate'
23
+
24
+ class AwsDurableExecutionSdkJsHandlerPlugin extends TracingPlugin {
25
+ static id = 'aws-durable-execution-sdk-js'
26
+ static type = 'serverless'
27
+ static kind = 'internal'
28
+ static prefix = 'tracing:orchestrion:@aws/durable-execution-sdk-js:withDurableExecution'
29
+
30
+ constructor (...args) {
31
+ super(...args)
32
+ // Gate the subscription on the feature flag: the instrumentation only wraps terminate() while
33
+ // this channel has subscribers, so not subscribing keeps the wrapping off entirely.
34
+ if (this._tracerConfig.DD_DURABLE_CROSS_INVOCATION_TRACING_ENABLED) {
35
+ this.addSub(TERMINATE_CHANNEL, ctx => this.#onTerminate(ctx))
36
+ }
37
+ }
38
+
39
+ bindStart (ctx) {
40
+ const args = ctx.arguments || []
41
+ const event = args[0]
42
+ const durableExecutionMode = args[3]
43
+ const handler = args[5]
44
+
45
+ const meta = {
46
+ 'aws.durable.replayed': durableExecutionMode === 'ReplayMode' ? 'true' : 'false',
47
+ }
48
+ const arn = event?.DurableExecutionArn
49
+ if (arn) {
50
+ meta['aws.durable.execution_arn'] = arn
51
+ }
52
+
53
+ this.startSpan(this.operationName(), {
54
+ resource: handler?.name,
55
+ kind: this.constructor.kind,
56
+ meta,
57
+ }, ctx)
58
+
59
+ return ctx.currentStore
60
+ }
61
+
62
+ // Fired (synchronously, before the SDK's terminate() runs) when the execution suspends. On a
63
+ // PENDING reason we persist the current trace context as a `_datadog` checkpoint, which
64
+ // subsequent invocations consume to extract the parent trace context. `ctx` is the shared
65
+ // withDurableExecution context: bindStart put the execute span on it, and the instrumentation
66
+ // put the captured durableContext and termination reason on it.
67
+ #onTerminate (ctx) {
68
+ const reason = ctx.terminationReason ?? DEFAULT_TERMINATION_REASON
69
+ if (!PENDING_TERMINATION_REASONS.has(reason)) return
70
+ void maybeSaveCheckpoint(this.tracer, ctx)
71
+ }
72
+
73
+ asyncEnd (ctx) {
74
+ const span = ctx?.currentStore?.span
75
+ const status = ctx?.result?.Status
76
+ if (span && typeof status === 'string') {
77
+ span.setTag('aws.durable.invocation_status', status.toLowerCase())
78
+ }
79
+ // Operation child spans rely on user code awaiting the returned DurablePromise to settle;
80
+ // suspended (PENDING) ops never settle, and fire-and-forget ops on terminal handler exits
81
+ // are never awaited at all. Finish any still-open owned children so the trace can flush.
82
+ if (span) finishOpenChildSpans(span)
83
+ super.finish(ctx)
84
+ }
85
+ }
86
+
87
+ function finishOpenChildSpans (executeSpan) {
88
+ const trace = executeSpan?._spanContext?._trace
89
+ if (!trace?.started) return
90
+
91
+ for (const span of trace.started) {
92
+ if (span === executeSpan) continue
93
+ if (span._integrationName !== AwsDurableExecutionSdkJsHandlerPlugin.id) continue
94
+ if (span._duration === undefined) {
95
+ span.finish()
96
+ }
97
+ }
98
+ }
99
+
100
+ // Save state is kept on the shared `ctx` so repeated terminate() calls within one execution
101
+ // save at most once. The execute span is also the anchor we propagate, so its span id is the
102
+ // `firstExecutionSpanId` passed downstream.
103
+ function maybeSaveCheckpoint (tracer, ctx) {
104
+ if (ctx.checkpointSaved || ctx.checkpointSavePromise) return ctx.checkpointSavePromise
105
+
106
+ const span = ctx.currentStore?.span
107
+ const durableContext = ctx.durableContext
108
+ if (!span || !durableContext) return
109
+
110
+ // Fire-and-forget boundary (#onTerminate calls us with `void`): swallow every failure here so a
111
+ // rejected checkpoint-manager call can never surface as an unhandled rejection in customer code.
112
+ ctx.checkpointSavePromise = saveTraceContextCheckpointIfUpdated(
113
+ tracer,
114
+ span,
115
+ durableContext,
116
+ span.context?.()?.toSpanId?.(),
117
+ ctx.arguments?.[0],
118
+ ).catch(error => {
119
+ log.debug('Failed to save trace context checkpoint', error)
120
+ }).finally(() => {
121
+ ctx.checkpointSaved = true
122
+ ctx.checkpointSavePromise = undefined
123
+ })
124
+
125
+ return ctx.checkpointSavePromise
126
+ }
127
+
128
+ module.exports = AwsDurableExecutionSdkJsHandlerPlugin
@@ -0,0 +1,19 @@
1
+ 'use strict'
2
+
3
+ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
4
+ const checkpointPlugin = require('./checkpoint')
5
+ const clientPlugin = require('./client')
6
+ const contextPlugins = require('./context')
7
+ const handlerPlugin = require('./handler')
8
+
9
+ class AwsDurableExecutionSdkJsPlugin extends CompositePlugin {
10
+ static id = 'aws-durable-execution-sdk-js'
11
+ static plugins = {
12
+ handler: handlerPlugin,
13
+ client: clientPlugin,
14
+ checkpoint: checkpointPlugin,
15
+ ...contextPlugins,
16
+ }
17
+ }
18
+
19
+ module.exports = AwsDurableExecutionSdkJsPlugin
@@ -0,0 +1,224 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('crypto')
4
+ const log = require('../../dd-trace/src/log')
5
+ const TextMapPropagator = require('../../dd-trace/src/opentracing/propagation/text_map')
6
+
7
+ const CHECKPOINT_NAME_PREFIX = '_datadog_'
8
+
9
+ // Propagator that injects only Datadog-style headers (`x-datadog-*`) regardless of the user's
10
+ // `DD_TRACE_PROPAGATION_STYLE_INJECT`. Checkpoints are written and read entirely by Datadog code,
11
+ // so honoring user style preferences would only complicate the payload contract. AWS runs a single
12
+ // tracer, so one lazily-built propagator suffices.
13
+ let datadogOnlyPropagator
14
+
15
+ function getDatadogOnlyPropagator (tracer) {
16
+ if (datadogOnlyPropagator) return datadogOnlyPropagator
17
+ const config = tracer._config
18
+ // Shadow `tracePropagationStyle.inject` while inheriting every other field (x-datadog-tags length
19
+ // cap, etc.) from the live config. Disable `legacyBaggageEnabled` only to keep `ot-baggage-*` out
20
+ // of the checkpoint payload we persist (sensitive-data concern) — not a serverless-wide change.
21
+ const shadowConfig = Object.create(config)
22
+ shadowConfig.tracePropagationStyle = {
23
+ ...config.tracePropagationStyle,
24
+ inject: ['datadog'],
25
+ }
26
+ shadowConfig.legacyBaggageEnabled = false
27
+ datadogOnlyPropagator = new TextMapPropagator(shadowConfig)
28
+ return datadogOnlyPropagator
29
+ }
30
+
31
+ /**
32
+ * Build the Datadog-format headers dict from a span context.
33
+ * Mirrors ddtrace-py HTTPPropagator.inject output so the same payload
34
+ * can be consumed by either language's datadog-lambda wrapper.
35
+ * @param {object} tracer
36
+ * @param {object} span
37
+ * @returns {Record<string, string>}
38
+ */
39
+ function injectHeaders (tracer, span) {
40
+ const headers = {}
41
+ const propagator = getDatadogOnlyPropagator(tracer)
42
+ const ctx = typeof span?.context === 'function' ? span.context() : span
43
+ propagator.inject(ctx, headers)
44
+ return headers
45
+ }
46
+
47
+ /**
48
+ * Mutate headers in-place to set parent_id to the provided value.
49
+ * @param {Record<string, string>} headers
50
+ * @param {string | number | undefined} parentId
51
+ */
52
+ function overrideParentId (headers, parentId) {
53
+ if (!parentId) return
54
+ if (headers['x-datadog-trace-id']) {
55
+ headers['x-datadog-parent-id'] = String(parentId)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Whether the current trace context warrants a new checkpoint over the previously-saved one.
61
+ * @param {Record<string, string>} currentHeaders
62
+ * @param {Record<string, string>} previousHeaders
63
+ * @returns {boolean}
64
+ */
65
+ function needsCheckpointUpdate (currentHeaders, previousHeaders) {
66
+ for (const key of Object.keys(currentHeaders)) {
67
+ if (currentHeaders[key] !== previousHeaders[key]) return true
68
+ }
69
+ return false
70
+ }
71
+
72
+ /**
73
+ * Find the checkpoint with the highest N for _datadog_{N} in the event's operations.
74
+ * @param {unknown} event
75
+ * @returns {{ checkpointNumber: number, operation: object } | undefined}
76
+ */
77
+ function findLastCheckpoint (event) {
78
+ if (!event || typeof event !== 'object') return
79
+
80
+ const operations = event.InitialExecutionState?.Operations
81
+ if (!Array.isArray(operations)) return
82
+
83
+ let highest
84
+ for (const op of operations) {
85
+ const name = op?.Name
86
+ if (typeof name !== 'string' || !name.startsWith(CHECKPOINT_NAME_PREFIX)) continue
87
+ const suffix = name.slice(CHECKPOINT_NAME_PREFIX.length)
88
+ const checkpointNumber = Number.parseInt(suffix, 10)
89
+ if (Number.isNaN(checkpointNumber) || String(checkpointNumber) !== suffix) continue
90
+
91
+ if (!highest || checkpointNumber > highest.checkpointNumber) {
92
+ highest = { checkpointNumber, operation: op }
93
+ }
94
+ }
95
+
96
+ return highest
97
+ }
98
+
99
+ /**
100
+ * Parse the JSON payload from a checkpoint STEP operation's Payload or StepDetails.Result.
101
+ * @param {object} op
102
+ * @returns {Record<string, string> | undefined}
103
+ */
104
+ function parseCheckpointPayload (op) {
105
+ try {
106
+ const raw = op?.Payload ?? op?.StepDetails?.Result
107
+ if (!raw || typeof raw !== 'string') return
108
+ const parsed = JSON.parse(raw)
109
+ return parsed && typeof parsed === 'object' ? parsed : undefined
110
+ } catch {
111
+ log.debug('Failed to parse checkpoint payload')
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Save a _datadog_{number} STEP operation via the SDK's checkpoint manager.
117
+ * Uses a deterministic blake2b hash of (name:arn) as the stepId so the save is
118
+ * idempotent within an execution.
119
+ * @param {object} checkpointManager
120
+ * @param {string} executionArn
121
+ * @param {number} number
122
+ * @param {Record<string, string>} headers
123
+ * @returns {Promise<void>}
124
+ */
125
+ async function saveCheckpoint (checkpointManager, executionArn, number, headers) {
126
+ const name = `${CHECKPOINT_NAME_PREFIX}${number}`
127
+ const stepId = crypto
128
+ .createHash('blake2b512')
129
+ .update(`${name}:${executionArn}`)
130
+ .digest('hex')
131
+ .slice(0, 64)
132
+ const payload = JSON.stringify(headers)
133
+
134
+ // Queue START and SUCCEED back-to-back before awaiting. This allows callers
135
+ // to trigger save right before termination without losing the second update.
136
+ const startPromise = checkpointManager.checkpoint(stepId, {
137
+ Id: stepId,
138
+ Action: 'START',
139
+ Type: 'STEP',
140
+ SubType: 'STEP',
141
+ Name: name,
142
+ })
143
+ const succeedPromise = checkpointManager.checkpoint(stepId, {
144
+ Id: stepId,
145
+ Action: 'SUCCEED',
146
+ Type: 'STEP',
147
+ SubType: 'STEP',
148
+ Name: name,
149
+ Payload: payload,
150
+ })
151
+ await Promise.all([
152
+ startPromise,
153
+ succeedPromise,
154
+ ])
155
+ log.debug('Saved trace context checkpoint %s', name)
156
+ }
157
+
158
+ /**
159
+ * Save a new trace-context checkpoint when the current context differs from
160
+ * the most recent `_datadog_{N}` operation already in the event.
161
+ *
162
+ * Every checkpoint across the durable execution carries the same
163
+ * `x-datadog-parent-id` so all resumed invocations attach to the same anchor:
164
+ * - First checkpoint (no previous): anchor at `firstExecutionSpanId`.
165
+ * - Subsequent: reuse the prior checkpoint's `x-datadog-parent-id` verbatim —
166
+ * that value originated from the first save and is the anchor we've been
167
+ * carrying forward.
168
+ *
169
+ * Caller is responsible for invoking this only when a save is appropriate — i.e.
170
+ * the SDK is about to return Status: PENDING (see PENDING_TERMINATION_REASONS in
171
+ * handler.js). This function does not re-check that.
172
+ *
173
+ * Errors (including a rejected checkpoint-manager call) propagate to the caller, which swallows
174
+ * them at the fire-and-forget boundary.
175
+ *
176
+ * @param {object} tracer
177
+ * @param {object} span - aws.durable.execute span
178
+ * @param {object} durableContext - SDK's DurableContextImpl
179
+ * @param {string | undefined} firstExecutionSpanId - span id of the first
180
+ * invocation's `aws.durable.execute` span. Only consulted on the very
181
+ * first save; ignored once a prior `_datadog_{N}` exists. We anchor at
182
+ * this span (which this integration owns) rather than its parent so the
183
+ * anchor doesn't depend on whatever upstream context happens to be
184
+ * active when `bindStart` fires.
185
+ * @param {unknown} event - raw invocation event (has InitialExecutionState)
186
+ * @returns {Promise<void>}
187
+ */
188
+ async function saveTraceContextCheckpointIfUpdated (
189
+ tracer, span, durableContext, firstExecutionSpanId, event,
190
+ ) {
191
+ const checkpointManager = durableContext.checkpoint ?? durableContext.checkpointManager
192
+ if (typeof checkpointManager?.checkpoint !== 'function') return
193
+
194
+ const currentHeaders = injectHeaders(tracer, span)
195
+ if (currentHeaders['x-datadog-trace-id'] === undefined) return
196
+
197
+ const latest = findLastCheckpoint(event)
198
+
199
+ let newNumber
200
+ if (latest) {
201
+ const previousHeaders = parseCheckpointPayload(latest.operation)
202
+ if (!previousHeaders) return
203
+
204
+ // x-datadog-parent-id reflects the active span at save time and always differs, so exclude it
205
+ // from the comparison. Capture the previous anchor first to carry it forward on a real update.
206
+ // needsCheckpointUpdate only reads currentHeaders' keys, so deleting it from there is enough.
207
+ const anchoredSpanId = previousHeaders['x-datadog-parent-id']
208
+ delete currentHeaders['x-datadog-parent-id']
209
+ if (!needsCheckpointUpdate(currentHeaders, previousHeaders)) return
210
+
211
+ newNumber = latest.checkpointNumber + 1
212
+ overrideParentId(currentHeaders, anchoredSpanId)
213
+ } else {
214
+ newNumber = 0
215
+ if (firstExecutionSpanId) overrideParentId(currentHeaders, firstExecutionSpanId)
216
+ }
217
+
218
+ const executionArn = event?.DurableExecutionArn || ''
219
+ await saveCheckpoint(checkpointManager, executionArn, newNumber, currentHeaders)
220
+ }
221
+
222
+ module.exports = {
223
+ saveTraceContextCheckpointIfUpdated,
224
+ }
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ const { createHash } = require('node:crypto')
4
+
5
+ /**
6
+ * Populates the replay and operation_id tags for the op the DurableContextImpl is about to
7
+ * run, deriving both from a single `getNextStepId()` call. `aws.durable.replayed` is always
8
+ * set ('true' when the next stepId already has a SUCCEEDED checkpoint entry, i.e. the op will
9
+ * be served from the SDK's checkpoint). `aws.durable.operation_id` — the 16-hex-char MD5 of
10
+ * the stepId, mirroring the SDK's internal calculation — is only added when a stepId exists.
11
+ * @param {Record<string, string>} meta - The span meta/tags object to populate.
12
+ * @param {object} [ctxImpl] - The DurableContextImpl about to run the op.
13
+ * @returns {void}
14
+ */
15
+ function addOpMeta (meta, ctxImpl) {
16
+ const stepId = ctxImpl?.getNextStepId?.()
17
+ if (!stepId) {
18
+ meta['aws.durable.replayed'] = 'false'
19
+ return
20
+ }
21
+ const stepData = ctxImpl?._executionContext?.getStepData?.(stepId)
22
+ meta['aws.durable.replayed'] = String(stepData?.Status === 'SUCCEEDED')
23
+ meta['aws.durable.operation_id'] = createHash('md5').update(stepId).digest('hex').slice(0, 16)
24
+ }
25
+
26
+ /**
27
+ * The SDK wraps user errors in typed classes (StepError, ChildContextError, etc.); we follow the
28
+ * `.cause` chain to recover the user's original Error. SDK wrappers expose a string `errorType`
29
+ * field, so the loop stops once we leave the wrapper hierarchy.
30
+ * @param {{ error?: unknown }} ctx
31
+ * @returns {unknown} the unwrapped error, or `ctx.error` unchanged when it isn't an Error
32
+ */
33
+ function unwrapDurableError (ctx) {
34
+ let err = ctx?.error
35
+ if (!(err instanceof Error)) return err
36
+
37
+ while (typeof err.errorType === 'string' && err.cause instanceof Error) {
38
+ err = err.cause
39
+ }
40
+ return err
41
+ }
42
+
43
+ module.exports = { addOpMeta, unwrapDurableError }
@@ -108,7 +108,7 @@ class BaseAwsSdkPlugin extends ClientPlugin {
108
108
  const span = this.startSpan(this.operationFromRequest(request), {
109
109
  childOf,
110
110
  meta,
111
- service: this.serviceName(),
111
+ service: this.serviceName(request),
112
112
  integrationName: 'aws-sdk',
113
113
  }, ctx)
114
114
 
@@ -172,18 +172,12 @@ class BaseAwsSdkPlugin extends ClientPlugin {
172
172
  })
173
173
 
174
174
  this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ctx => {
175
- const { response, cbExists = false, currentStore } = ctx
175
+ const { response, currentStore } = ctx
176
176
  if (!currentStore) return
177
177
  const { span } = currentStore
178
178
  if (!span) return
179
179
 
180
180
  storage('legacy').run(currentStore, () => {
181
- // try to extract DSM context from response if no callback exists as extraction normally happens in CB
182
- if (!cbExists && this.serviceIdentifier === 'sqs') {
183
- const params = response.request.params
184
- const operation = response.request.operation
185
- this.responseExtractDSMContext(operation, params, response.data ?? response, span)
186
- }
187
181
  this.addResponseTags(span, response)
188
182
 
189
183
  if (this._tracerConfig?.DD_TRACE_AWS_ADD_SPAN_POINTERS) {
@@ -222,14 +216,18 @@ class BaseAwsSdkPlugin extends ClientPlugin {
222
216
  })
223
217
  }
224
218
 
225
- serviceName () {
226
- return this.config.service ||
227
- super.serviceName({
228
- id: 'aws',
229
- type: 'web',
230
- kind: 'client',
231
- awsService: this.serviceIdentifier,
232
- })
219
+ /**
220
+ * @param {{ params?: object }} request
221
+ */
222
+ serviceName (request) {
223
+ return super.serviceName({
224
+ id: 'aws',
225
+ type: 'web',
226
+ kind: 'client',
227
+ awsService: this.serviceIdentifier,
228
+ pluginConfig: this.config,
229
+ params: request.params,
230
+ })
233
231
  }
234
232
 
235
233
  isEnabled (request) {
@@ -2,6 +2,34 @@
2
2
  const log = require('../../../dd-trace/src/log')
3
3
  const BaseAwsSdkPlugin = require('../base')
4
4
 
5
+ // EventBridge enforces this limit over the whole PutEvents request (the sum of
6
+ // every entry), not over a single entry. 1 MiB == 1,048,576 bytes.
7
+ const MAX_PUT_EVENTS_BYTES = 1024 * 1024
8
+
9
+ /**
10
+ * Size a single `PutEventsRequestEntry` the way EventBridge does server-side:
11
+ * the UTF-8 byte length of `Source`, `DetailType`, `Detail`, and each
12
+ * `Resources` ARN, plus a flat 14 bytes when `Time` is set.
13
+ *
14
+ * @param {object} entry a single PutEvents request entry
15
+ * @param {string} [detail] overrides `entry.Detail`, used to size the entry as
16
+ * it would be sent with the injected `_datadog` context
17
+ * @returns {number}
18
+ * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevents.html
19
+ */
20
+ function putEventEntrySize (entry, detail = entry.Detail) {
21
+ let size = entry.Time == null ? 0 : 14
22
+ if (entry.Source != null) size += Buffer.byteLength(entry.Source)
23
+ if (entry.DetailType != null) size += Buffer.byteLength(entry.DetailType)
24
+ if (detail != null) size += Buffer.byteLength(detail)
25
+ if (entry.Resources != null) {
26
+ for (const resource of entry.Resources) {
27
+ if (resource != null) size += Buffer.byteLength(resource)
28
+ }
29
+ }
30
+ return size
31
+ }
32
+
5
33
  class EventBridge extends BaseAwsSdkPlugin {
6
34
  static id = 'eventbridge'
7
35
  static isPayloadReporter = true
@@ -25,7 +53,8 @@ class EventBridge extends BaseAwsSdkPlugin {
25
53
  * Docs: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEventsRequestEntry.html
26
54
  * We cannot use the traceHeader field as that's reserved for X-Ray.
27
55
  * Detail must be a valid JSON string
28
- * Max size per event is 256kb (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevent-size.html)
56
+ * Max PutEvents request size is 1mb, summed over all entries
57
+ * (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevent-size.html)
29
58
  */
30
59
  requestInject (span, request) {
31
60
  const operation = request.operation
@@ -34,6 +63,7 @@ class EventBridge extends BaseAwsSdkPlugin {
34
63
  request.params.Entries &&
35
64
  request.params.Entries.length > 0 &&
36
65
  request.params.Entries[0].Detail) {
66
+ const entries = request.params.Entries
37
67
  const injected = {}
38
68
  this.tracer.inject(span, 'text_map', injected)
39
69
 
@@ -42,19 +72,26 @@ class EventBridge extends BaseAwsSdkPlugin {
42
72
  // it so the rest of the body stays in V8's optimisable surface.
43
73
  let finalData
44
74
  try {
45
- finalData = BaseAwsSdkPlugin.injectFieldIntoJsonObject(
46
- request.params.Entries[0].Detail, '_datadog', injected
47
- )
75
+ finalData = BaseAwsSdkPlugin.injectFieldIntoJsonObject(entries[0].Detail, '_datadog', injected)
48
76
  } catch (error) {
49
77
  log.error('EventBridge error injecting request', error)
50
78
  return
51
79
  }
52
80
 
53
- if (Buffer.byteLength(finalData) >= 1024 * 256) {
81
+ // EventBridge applies the 1 MiB cap to the whole request, so size every
82
+ // entry as it would be sent (the first with `_datadog` injected) and skip
83
+ // rather than tip a request AWS would otherwise accept over the limit. The
84
+ // running total only needs to clear the cap, so stop summing the moment it
85
+ // does instead of byte-counting the rest of a batch we already know is over.
86
+ let requestSize = putEventEntrySize(entries[0], finalData)
87
+ for (let i = 1; requestSize < MAX_PUT_EVENTS_BYTES && i < entries.length; i++) {
88
+ requestSize += putEventEntrySize(entries[i])
89
+ }
90
+ if (requestSize >= MAX_PUT_EVENTS_BYTES) {
54
91
  log.info('Payload size too large to pass context')
55
92
  return
56
93
  }
57
- request.params.Entries[0].Detail = finalData
94
+ entries[0].Detail = finalData
58
95
  }
59
96
  }
60
97
  }