dd-trace 5.107.0 → 5.109.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 (66) hide show
  1. package/index.d.ts +22 -1
  2. package/package.json +6 -5
  3. package/packages/datadog-instrumentations/src/ai.js +43 -48
  4. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  5. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  7. package/packages/datadog-instrumentations/src/electron.js +1 -1
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  11. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  12. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  13. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  14. package/packages/datadog-instrumentations/src/next.js +34 -0
  15. package/packages/datadog-instrumentations/src/openai.js +77 -18
  16. package/packages/datadog-instrumentations/src/redis.js +0 -1
  17. package/packages/datadog-instrumentations/src/vitest.js +60 -1
  18. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  19. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  20. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  21. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  23. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  24. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  25. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
  26. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  27. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
  28. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  29. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  30. package/packages/datadog-plugin-fs/src/index.js +1 -0
  31. package/packages/datadog-plugin-redis/src/index.js +1 -2
  32. package/packages/datadog-plugin-vitest/src/index.js +4 -1
  33. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  34. package/packages/dd-trace/src/aiguard/index.js +11 -49
  35. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  36. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  37. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  38. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  39. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  40. package/packages/dd-trace/src/appsec/channels.js +1 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +114 -60
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  45. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  46. package/packages/dd-trace/src/appsec/rasp/ssrf.js +21 -12
  47. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  48. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  49. package/packages/dd-trace/src/config/supported-configurations.json +31 -2
  50. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
  51. package/packages/dd-trace/src/dogstatsd.js +15 -8
  52. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  53. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  54. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  55. package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
  57. package/packages/dd-trace/src/plugins/index.js +3 -0
  58. package/packages/dd-trace/src/profiling/config.js +2 -0
  59. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  60. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  61. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  62. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  63. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  64. package/vendor/dist/protobufjs/index.js +1 -1
  65. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  66. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
@@ -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 }
@@ -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) {
@@ -8,11 +8,18 @@ function recordDataAsString (data) {
8
8
  return Buffer.isBuffer(data) ? data.toString('utf8') : Buffer.from(data).toString('utf8')
9
9
  }
10
10
 
11
+ // Caps the promise-path iterator→stream cache so abandoned shard iterators
12
+ // (AWS expires them after 5 minutes) can't grow it without bound. Polling loops
13
+ // delete on consume, so their working set is ~the active shard count.
14
+ const MAX_TRACKED_SHARD_ITERATORS = 1000
15
+
11
16
  class Kinesis extends BaseAwsSdkPlugin {
12
17
  static id = 'kinesis'
13
18
  static peerServicePrecursors = ['streamname']
14
19
  static isPayloadReporter = true
15
20
 
21
+ #shardIteratorStreams = new Map()
22
+
16
23
  constructor (...args) {
17
24
  super(...args)
18
25
 
@@ -20,51 +27,107 @@ class Kinesis extends BaseAwsSdkPlugin {
20
27
  // in the base class
21
28
  this.requestTags = new WeakMap()
22
29
 
23
- this.addBind('apm:aws:response:start:kinesis', ctx => {
24
- const { request, response } = ctx
25
- const plugin = this
30
+ this.addBind('apm:aws:response:start:kinesis', ctx => this.#startResponseSpan(ctx))
31
+
32
+ // Promise / event-emitter calls never publish response:start, so create and finish the
33
+ // consumer span from request:complete instead. Callback calls handle it via the bind above.
34
+ this.addSub('apm:aws:request:complete:kinesis', ctx => {
35
+ if (ctx.cbExists) return
36
+ // v2 nests the SDK payload under response.data; v3 spreads the output onto response.
37
+ const response = ctx.response?.data ?? ctx.response
38
+ const responseCtx = { request: ctx.request, response }
39
+ this.#startResponseSpan(responseCtx)
40
+ if (responseCtx.needsFinish) this.finish(responseCtx)
41
+ // The async store that carries streamName to getRecords on the callback path is
42
+ // absent here, so map each shard iterator to its stream for the DSM topic tag.
43
+ if (this.config.dsmEnabled) this.#trackShardStream(ctx.request, response)
44
+ })
26
45
 
27
- let store = this._parentMap.get(request)
46
+ this.addSub('apm:aws:response:finish:kinesis', ctx => {
47
+ if (!ctx.needsFinish) return
48
+ this.finish(ctx)
49
+ })
50
+ }
28
51
 
29
- // if we have either of these operations, we want to store the streamName param
30
- // since it is not typically available during get/put records requests
31
- if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
32
- return this.storeStreamName(request.params, request.operation, store)
33
- }
52
+ /**
53
+ * @param {object} ctx Completion context carrying the SDK request and response.
54
+ */
55
+ #startResponseSpan (ctx) {
56
+ const { request, response } = ctx
34
57
 
35
- if (request.operation === 'getRecords') {
36
- let span
37
- const responseExtraction = this.responseExtract(request.params, request.operation, response)
38
- if (responseExtraction && responseExtraction.maybeChildOf) {
39
- ctx.needsFinish = true
40
- const options = {
41
- childOf: responseExtraction.maybeChildOf,
42
- meta: {
43
- ...this.requestTags.get(request),
44
- 'span.kind': 'server',
45
- },
46
- integrationName: 'aws-sdk',
47
- }
48
- span = plugin.startSpan('aws.response', options, ctx)
49
- store = ctx.currentStore
50
- }
58
+ let store = this._parentMap.get(request)
51
59
 
52
- // get the stream name that should have been stored previously
53
- const { streamName } = store
60
+ // if we have either of these operations, we want to store the streamName param
61
+ // since it is not typically available during get/put records requests
62
+ if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
63
+ return this.storeStreamName(request.params, request.operation, store)
64
+ }
54
65
 
55
- // extract DSM context after as we might not have a parent-child but may have a DSM context
56
- this.responseExtractDSMContext(
57
- request.operation, request.params, response, span || null, { streamName }
58
- )
66
+ if (request.operation === 'getRecords') {
67
+ let span
68
+ const responseExtraction = this.responseExtract(request.params, request.operation, response)
69
+ if (responseExtraction && responseExtraction.maybeChildOf) {
70
+ ctx.needsFinish = true
71
+ const options = {
72
+ childOf: responseExtraction.maybeChildOf,
73
+ meta: {
74
+ ...this.requestTags.get(request),
75
+ 'span.kind': 'server',
76
+ },
77
+ integrationName: 'aws-sdk',
78
+ }
79
+ span = this.startSpan('aws.response', options, ctx)
80
+ store = ctx.currentStore
59
81
  }
60
82
 
61
- return store
62
- })
83
+ if (this.config.dsmEnabled) {
84
+ // streamName rides the async store on the callback path; the promise path has no
85
+ // such link, so fall back to the iterator the producer returned.
86
+ const streamName = store?.streamName ?? this.#shardIteratorStreams.get(request.params.ShardIterator)
87
+ this.responseExtractDSMContext(request.operation, request.params, response, span || null, { streamName })
88
+ }
89
+ }
63
90
 
64
- this.addSub('apm:aws:response:finish:kinesis', ctx => {
65
- if (!ctx.needsFinish) return
66
- this.finish(ctx)
67
- })
91
+ return store
92
+ }
93
+
94
+ /**
95
+ * @param {object} request SDK request; reads `operation` and `params`.
96
+ * @param {object} response SDK output; reads `ShardIterator` / `NextShardIterator`.
97
+ */
98
+ #trackShardStream (request, response) {
99
+ if (request.operation === 'getShardIterator') {
100
+ this.#rememberShardStream(response?.ShardIterator, request.params?.StreamName)
101
+ } else if (request.operation === 'getRecords') {
102
+ this.#advanceShardStream(request.params?.ShardIterator, response?.NextShardIterator)
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {string} [iterator] Shard iterator the producer returned.
108
+ * @param {string} [streamName] Stream the iterator belongs to.
109
+ */
110
+ #rememberShardStream (iterator, streamName) {
111
+ if (!iterator || streamName === undefined) return
112
+ // FIFO-evict the oldest entry (Map keeps insertion order) when the cap is hit; only
113
+ // abandoned iterators get here, so no realistic test drives the cap (eviction ignored).
114
+ /* istanbul ignore if */
115
+ if (this.#shardIteratorStreams.size >= MAX_TRACKED_SHARD_ITERATORS) {
116
+ this.#shardIteratorStreams.delete(this.#shardIteratorStreams.keys().next().value)
117
+ }
118
+ this.#shardIteratorStreams.set(iterator, streamName)
119
+ }
120
+
121
+ /**
122
+ * @param {string} [consumedIterator] Iterator just passed to getRecords.
123
+ * @param {string} [nextIterator] NextShardIterator for the following poll.
124
+ */
125
+ #advanceShardStream (consumedIterator, nextIterator) {
126
+ const streamName = this.#shardIteratorStreams.get(consumedIterator)
127
+ if (streamName === undefined) return
128
+ this.#shardIteratorStreams.delete(consumedIterator)
129
+ // carry the stream onto the next iterator so the polling loop keeps its topic
130
+ if (nextIterator) this.#rememberShardStream(nextIterator, streamName)
68
131
  }
69
132
 
70
133
  generateTags (params, operation, response) {
@@ -54,47 +54,64 @@ class Sqs extends BaseAwsSdkPlugin {
54
54
  // in the base class
55
55
  this.requestTags = new WeakMap()
56
56
 
57
- this.addBind('apm:aws:response:start:sqs', ctx => {
58
- const { request, response } = ctx
59
- const contextExtraction = this.responseExtract(request.params, request.operation, response)
60
-
61
- let store = this._parentMap.get(request)
62
- let span
63
- let parsedMessageAttributes
64
- let parsedFirstBody
65
- let firstBodyChecked = false
66
- if (contextExtraction !== undefined) {
67
- parsedFirstBody = contextExtraction.parsedBody
68
- firstBodyChecked = contextExtraction.bodyChecked === true
69
- if (contextExtraction.datadogContext !== undefined) {
57
+ this.addBind('apm:aws:response:start:sqs', ctx => this.#startResponseSpan(ctx))
58
+
59
+ // No-callback receives (promises, event emitters) never publish response:start, so link and
60
+ // finish the consumer span here instead. Callback paths reach the same logic via the bind above.
61
+ this.addSub('apm:aws:request:complete:sqs', ctx => {
62
+ if (ctx.cbExists) return
63
+ // v2 nests the SDK payload under response.data; v3 spreads the output onto response.
64
+ const responseCtx = { request: ctx.request, response: ctx.response?.data ?? ctx.response }
65
+ this.#startResponseSpan(responseCtx)
66
+ if (responseCtx.needsFinish) this.finish(responseCtx)
67
+ })
68
+
69
+ this.addSub('apm:aws:response:finish:sqs', ctx => {
70
+ if (!ctx.needsFinish) return
71
+ this.finish(ctx)
72
+ })
73
+ }
74
+
75
+ #startResponseSpan (ctx) {
76
+ const { request, response } = ctx
77
+ const contextExtraction = this.responseExtract(request.params, request.operation, response)
78
+
79
+ let store = this._parentMap.get(request)
80
+ let span
81
+ let parsedMessageAttributes
82
+ let parsedFirstBody
83
+ let firstBodyChecked = false
84
+ if (contextExtraction !== undefined) {
85
+ parsedFirstBody = contextExtraction.parsedBody
86
+ firstBodyChecked = contextExtraction.bodyChecked === true
87
+ if (contextExtraction.datadogContext !== undefined) {
88
+ parsedMessageAttributes = contextExtraction.parsedAttributes
89
+ // request:start records requestTags only after the isEnabled gate, so an absent entry
90
+ // means this consumer is disabled — gate on it instead of paying isEnabled again here.
91
+ const requestTags = this.requestTags.get(request)
92
+ if (requestTags !== undefined) {
70
93
  ctx.needsFinish = true
71
94
  const options = {
72
95
  childOf: contextExtraction.datadogContext,
73
96
  meta: {
74
- ...this.requestTags.get(request),
97
+ ...requestTags,
75
98
  'span.kind': 'server',
76
99
  },
77
100
  integrationName: 'aws-sdk',
78
101
  }
79
- parsedMessageAttributes = contextExtraction.parsedAttributes
80
102
  span = this.startSpan('aws.response', options, ctx)
81
103
  store = ctx.currentStore
82
104
  }
83
105
  }
106
+ }
84
107
 
85
- // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
86
- this.responseExtractDSMContext(
87
- request.operation, request.params, response, span ?? null,
88
- { parsedAttributes: parsedMessageAttributes, parsedFirstBody, firstBodyChecked }
89
- )
90
-
91
- return store
92
- })
108
+ // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
109
+ this.responseExtractDSMContext(
110
+ request.operation, request.params, response, span ?? null,
111
+ { parsedAttributes: parsedMessageAttributes, parsedFirstBody, firstBodyChecked }
112
+ )
93
113
 
94
- this.addSub('apm:aws:response:finish:sqs', ctx => {
95
- if (!ctx.needsFinish) return
96
- this.finish(ctx)
97
- })
114
+ return store
98
115
  }
99
116
 
100
117
  operationFromRequest (request) {
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../dd-trace/src/log')
4
+
5
+ /**
6
+ * @typedef {object} BullmqJobShape
7
+ * @property {string} [name]
8
+ * @property {unknown} [data]
9
+ * @property {unknown} [opts]
10
+ * @property {string} [queueName]
11
+ */
12
+
13
+ /**
14
+ * @typedef {(job: BullmqJobShape) => boolean} BullmqFilter
15
+ */
16
+
17
+ /**
18
+ * Resolve a user-provided filter from plugin config. Returns `undefined` when
19
+ * no filter is configured so callers can short-circuit the filtering path on a
20
+ * cheap truthy check. If `producerFilter` is present but not a function, log
21
+ * an error and fall back to no filter.
22
+ *
23
+ * @param {{ producerFilter?: unknown }} config Plugin config that may carry a `producerFilter`.
24
+ * @returns {BullmqFilter | undefined}
25
+ */
26
+ function getFilter (config) {
27
+ if (typeof config?.producerFilter === 'function') {
28
+ return /** @type {BullmqFilter} */ (config.producerFilter)
29
+ }
30
+ if (config?.producerFilter !== undefined) {
31
+ log.error('Expected `producerFilter` to be a function. Ignoring.')
32
+ }
33
+ }
34
+
35
+ module.exports = { getFilter }