dd-trace 5.108.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 (58) hide show
  1. package/index.d.ts +22 -1
  2. package/package.json +2 -1
  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 +111 -58
  42. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  45. package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
  46. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
  47. package/packages/dd-trace/src/config/supported-configurations.json +24 -2
  48. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
  49. package/packages/dd-trace/src/dogstatsd.js +15 -8
  50. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  51. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  52. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  53. package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
  54. package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
  55. package/packages/dd-trace/src/plugins/index.js +3 -0
  56. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  57. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  58. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
@@ -3,11 +3,52 @@
3
3
  const dc = require('dc-polyfill')
4
4
  const shimmer = require('../../datadog-shimmer')
5
5
  const { addHook } = require('./helpers/instrument')
6
- const aiGuard = require('./helpers/openai-ai-guard')
7
6
 
8
7
  const ch = dc.tracingChannel('apm:openai:request')
9
8
  const onStreamedChunkCh = dc.channel('apm:openai:request:chunk')
10
9
 
10
+ // Provider lifecycle channels. Payloads stay OpenAI-native:
11
+ // before { args, parentSpan, abortController, pending }
12
+ // after { args, body, parentSpan, abortController, pending }
13
+ const chatCompletionsBeforeChannel = dc.channel('dd-trace:openai:chat.completions:before')
14
+ const chatCompletionsAfterChannel = dc.channel('dd-trace:openai:chat.completions:after')
15
+ const responsesBeforeChannel = dc.channel('dd-trace:openai:responses:before')
16
+ const responsesAfterChannel = dc.channel('dd-trace:openai:responses:after')
17
+
18
+ const LIFECYCLE_CHANNELS = {
19
+ 'chat.completions': {
20
+ before: chatCompletionsBeforeChannel,
21
+ after: chatCompletionsAfterChannel,
22
+ },
23
+ responses: {
24
+ before: responsesBeforeChannel,
25
+ after: responsesAfterChannel,
26
+ },
27
+ }
28
+
29
+ /**
30
+ * Publishes a provider-native lifecycle payload to a cancelable lifecycle channel.
31
+ *
32
+ * Subscribers push async work into `pending` synchronously during publication and
33
+ * abort `abortController` with an error before the pushed promise resolves to block.
34
+ *
35
+ * @param {object} channel
36
+ * @param {object} payload
37
+ * @returns {Promise<void>}
38
+ */
39
+ function publishLifecycle (channel, payload) {
40
+ const abortController = new AbortController()
41
+ const ctx = { ...payload, abortController, pending: [] }
42
+
43
+ channel.publish(ctx)
44
+
45
+ return Promise.all(ctx.pending).then(() => {
46
+ if (abortController.signal.aborted) {
47
+ throw abortController.signal.reason
48
+ }
49
+ })
50
+ }
51
+
11
52
  const V4_PACKAGE_SHIMS = [
12
53
  {
13
54
  file: 'resources/chat/completions',
@@ -217,17 +258,15 @@ for (const extension of extensions) {
217
258
 
218
259
  for (const methodName of methods) {
219
260
  shimmer.wrap(targetPrototype, methodName, methodFn => function (...args) {
220
- if (!ch.start.hasSubscribers && !aiGuard.hasSubscribers()) {
221
- return methodFn.apply(this, args)
222
- }
223
261
  // The OpenAI library lets you set `stream: true` on the options arg to any method
224
262
  // However, we only want to handle streamed responses in specific cases
225
263
  // chat.completions and completions
226
264
  const stream = streamedResponse && getOption(args, 'stream', false)
227
265
 
228
- const guard = aiGuard.createGuard(baseResource, args[0], stream)
266
+ const channels = stream ? null : LIFECYCLE_CHANNELS[baseResource]
267
+ const hasLifecycle = !!channels && (channels.before.hasSubscribers || channels.after.hasSubscribers)
229
268
 
230
- if (!ch.start.hasSubscribers && !guard) {
269
+ if (!ch.start.hasSubscribers && !hasLifecycle) {
231
270
  return methodFn.apply(this, args)
232
271
  }
233
272
 
@@ -240,12 +279,22 @@ for (const extension of extensions) {
240
279
  }
241
280
 
242
281
  return ch.start.runStores(ctx, () => {
243
- // Explicit childOf rather than async-context: the _thenUnwrap/parse path
244
- // decouples the lazy evaluation from the active scope at call time.
245
- if (guard) guard.parentSpan = ctx.currentStore?.span
282
+ // Capture the parent span explicitly: the _thenUnwrap/parse path decouples
283
+ // the lazy evaluation from the active scope at call time.
284
+ const parentSpan = hasLifecycle ? ctx.currentStore?.span : undefined
246
285
 
247
286
  const apiProm = methodFn.apply(this, args)
248
287
 
288
+ const beforeChannel = hasLifecycle && channels.before.hasSubscribers ? channels.before : null
289
+ const afterChannel = hasLifecycle && channels.after.hasSubscribers ? channels.after : null
290
+ let beforeVerdict
291
+ const getBeforeVerdict = beforeChannel
292
+ ? function getBeforeVerdict () {
293
+ beforeVerdict ??= publishLifecycle(beforeChannel, { args, parentSpan })
294
+ return beforeVerdict
295
+ }
296
+ : null
297
+
249
298
  if (baseResource === 'chat.completions' && typeof apiProm._thenUnwrap === 'function') {
250
299
  // this should only ever be invoked from a client.beta.chat.completions.parse call
251
300
  shimmer.wrap(apiProm, '_thenUnwrap', origApiPromThenUnwrap => function (...args) {
@@ -259,7 +308,9 @@ for (const extension of extensions) {
259
308
  const parsedPromise = origApiPromParse.apply(this, args)
260
309
  .then(body => Promise.all([this.responsePromise, body]))
261
310
 
262
- return handleUnwrappedAPIPromise(parsedPromise, ctx, stream, guard)
311
+ return handleUnwrappedAPIPromise(
312
+ parsedPromise, ctx, stream, getBeforeVerdict, afterChannel, parentSpan
313
+ )
263
314
  })
264
315
 
265
316
  return unwrappedPromise
@@ -272,10 +323,16 @@ for (const extension of extensions) {
272
323
  const parsedPromise = origApiPromParse.apply(this, args)
273
324
  .then(body => Promise.all([this.responsePromise, body]))
274
325
 
275
- return handleUnwrappedAPIPromise(parsedPromise, ctx, stream, guard)
326
+ return handleUnwrappedAPIPromise(parsedPromise, ctx, stream, getBeforeVerdict, afterChannel, parentSpan)
276
327
  })
277
328
 
278
- if (guard) aiGuard.wrapAsResponse(apiProm, guard)
329
+ // Gate `.asResponse()` callers on the before verdict so raw-response paths still block.
330
+ if (beforeChannel && typeof apiProm.asResponse === 'function') {
331
+ shimmer.wrap(apiProm, 'asResponse', origAsResponse => function (...args) {
332
+ const responsePromise = origAsResponse.apply(this, args)
333
+ return Promise.all([getBeforeVerdict(), responsePromise]).then(([, response]) => response)
334
+ })
335
+ }
279
336
 
280
337
  ch.end.publish(ctx)
281
338
 
@@ -288,10 +345,12 @@ for (const extension of extensions) {
288
345
  }
289
346
  }
290
347
 
291
- function handleUnwrappedAPIPromise (apiProm, ctx, stream, guard) {
292
- const guardedApiProm = guard ? aiGuard.gateParse(apiProm, guard) : apiProm
348
+ function handleUnwrappedAPIPromise (apiProm, ctx, stream, getBeforeVerdict, afterChannel, parentSpan) {
349
+ const gatedApiProm = getBeforeVerdict
350
+ ? Promise.all([getBeforeVerdict(), apiProm]).then(([, result]) => result)
351
+ : apiProm
293
352
 
294
- return guardedApiProm
353
+ return gatedApiProm
295
354
  .then(([{ response, options }, body]) => {
296
355
  if (stream) {
297
356
  if (body.iterator) {
@@ -313,14 +372,14 @@ function handleUnwrappedAPIPromise (apiProm, ctx, stream, guard) {
313
372
  },
314
373
  }
315
374
 
316
- if (!guard) {
375
+ if (!afterChannel) {
317
376
  finish(ctx, responseData)
318
377
  return body
319
378
  }
320
379
 
321
380
  // Finish after evaluation so a block propagates the error to openai.request
322
- // and the span wraps its ai_guard child instead of closing before it.
323
- return aiGuard.evaluateOutput(guard, body).then(() => {
381
+ // and the span wraps its child instead of closing before it.
382
+ return publishLifecycle(afterChannel, { args: ctx.args, body, parentSpan }).then(() => {
324
383
  finish(ctx, responseData)
325
384
  return body
326
385
  })
@@ -147,7 +147,6 @@ function getStartCtx (client, command, args, argsStartIndex) {
147
147
  }
148
148
 
149
149
  return {
150
- db: client.selected_db,
151
150
  command,
152
151
  args,
153
152
  argsStartIndex,
@@ -143,6 +143,8 @@ function getProvidedContext () {
143
143
  _ddTestSessionId: testSessionId,
144
144
  _ddTestModuleId: testModuleId,
145
145
  _ddTestCommand: testCommand,
146
+ _ddRepositoryRoot: repositoryRoot,
147
+ _ddCodeOwnersEntries: codeOwnersEntries,
146
148
  } = globalThis.__vitest_worker__.providedContext
147
149
 
148
150
  return {
@@ -162,6 +164,8 @@ function getProvidedContext () {
162
164
  testSessionId,
163
165
  testModuleId,
164
166
  testCommand,
167
+ repositoryRoot,
168
+ codeOwnersEntries,
165
169
  }
166
170
  } catch {
167
171
  log.error('Vitest workers could not parse provided context, so some features will not work.')
@@ -182,6 +186,8 @@ function getProvidedContext () {
182
186
  testSessionId: undefined,
183
187
  testModuleId: undefined,
184
188
  testCommand: undefined,
189
+ repositoryRoot: undefined,
190
+ codeOwnersEntries: undefined,
185
191
  }
186
192
  }
187
193
  }
@@ -472,7 +478,7 @@ async function runMainProcessSetup (ctx, frameworkVersion, testSpecifications) {
472
478
  }
473
479
 
474
480
  if (testSessionConfigurationCh.hasSubscribers) {
475
- const { testSessionId, testModuleId, testCommand } = await getChannelPromise(
481
+ const { testSessionId, testModuleId, testCommand, repositoryRoot, codeOwnersEntries } = await getChannelPromise(
476
482
  testSessionConfigurationCh,
477
483
  frameworkVersion
478
484
  )
@@ -480,6 +486,8 @@ async function runMainProcessSetup (ctx, frameworkVersion, testSpecifications) {
480
486
  _ddTestSessionId: testSessionId,
481
487
  _ddTestModuleId: testModuleId,
482
488
  _ddTestCommand: testCommand,
489
+ _ddRepositoryRoot: repositoryRoot,
490
+ _ddCodeOwnersEntries: codeOwnersEntries,
483
491
  }, 'Could not send test session configuration to workers.')
484
492
  }
485
493
 
@@ -653,15 +661,64 @@ function getCliOrStartVitestWrapper (frameworkVersion) {
653
661
  }
654
662
  }
655
663
 
664
+ function isForkPool (pool) {
665
+ return pool === 'forks' || pool === 'vmForks'
666
+ }
667
+
668
+ function isThreadPool (pool) {
669
+ return pool === 'threads' || pool === 'vmThreads'
670
+ }
671
+
672
+ function getTestSpecificationPool (testSpecification) {
673
+ const project = Array.isArray(testSpecification) ? testSpecification[0] : testSpecification?.project
674
+ return project?.config?.pool || project?.serializedConfig?.pool || project?.pool || testSpecification?.pool
675
+ }
676
+
677
+ function hasForkPoolTestSpecification (testSpecifications) {
678
+ if (!Array.isArray(testSpecifications)) {
679
+ return false
680
+ }
681
+
682
+ for (const testSpecification of testSpecifications) {
683
+ if (isForkPool(getTestSpecificationPool(testSpecification))) {
684
+ return true
685
+ }
686
+ }
687
+
688
+ return false
689
+ }
690
+
691
+ function shouldMarkVitestWorkerEnv (pool, testSpecifications) {
692
+ return isForkPool(pool) || hasForkPoolTestSpecification(testSpecifications) ||
693
+ (!testSpecifications && !isThreadPool(pool))
694
+ }
695
+
696
+ function markVitestWorkerEnv (ctx, testSpecifications) {
697
+ const config = ctx?.config
698
+ if (!config || !shouldMarkVitestWorkerEnv(config.pool, testSpecifications)) {
699
+ return
700
+ }
701
+ config.env = config.env || {}
702
+ config.env.DD_VITEST_WORKER = '1'
703
+ }
704
+
656
705
  function wrapVitestRunFiles (Vitest, frameworkVersion) {
657
706
  if (!Vitest?.prototype?.runFiles) {
658
707
  return
659
708
  }
660
709
 
661
710
  shimmer.wrap(Vitest.prototype, 'runFiles', runFiles => async function (testSpecifications) {
711
+ markVitestWorkerEnv(this, testSpecifications)
662
712
  await ensureMainProcessSetup(this, frameworkVersion, testSpecifications)
663
713
  return runFiles.apply(this, arguments)
664
714
  })
715
+
716
+ if (Vitest.prototype.collectTests) {
717
+ shimmer.wrap(Vitest.prototype, 'collectTests', collectTests => function () {
718
+ markVitestWorkerEnv(this)
719
+ return collectTests.apply(this, arguments)
720
+ })
721
+ }
665
722
  }
666
723
 
667
724
  function getCreateCliWrapper (vitestPackage, frameworkVersion) {
@@ -1393,6 +1450,8 @@ addHook({
1393
1450
  testSessionId: providedContext.testSessionId,
1394
1451
  testModuleId: providedContext.testModuleId,
1395
1452
  testCommand: providedContext.testCommand,
1453
+ repositoryRoot: providedContext.repositoryRoot,
1454
+ codeOwnersEntries: providedContext.codeOwnersEntries,
1396
1455
  }
1397
1456
  testSuiteStartCh.runStores(testSuiteCtx, () => {})
1398
1457
  const startTestsResponse = await startTests.apply(this, arguments)
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+
5
+ // On retries the SDK suspends execution without firing error/asyncEnd; finish the span here.
6
+ class AwsDurableExecutionSdkJsCheckpointPlugin extends TracingPlugin {
7
+ static id = 'aws-durable-execution-sdk-js'
8
+ static prefix = 'tracing:orchestrion:@aws/durable-execution-sdk-js:CheckpointManager_checkpoint'
9
+
10
+ start (ctx) {
11
+ const data = ctx.arguments?.[1]
12
+ if (data?.Action !== 'RETRY' || !data.Error) return
13
+
14
+ const span = this.activeSpan
15
+ if (!span || span.context().getTag('error')) return
16
+
17
+ const { ErrorMessage, ErrorType, StackTrace } = data.Error
18
+ span.setTag('error', 1)
19
+ if (ErrorMessage) span.setTag('error.message', ErrorMessage)
20
+ if (ErrorType) span.setTag('error.type', ErrorType)
21
+ if (Array.isArray(StackTrace)) span.setTag('error.stack', StackTrace.join('\n'))
22
+
23
+ ctx.retryStepSpan = span
24
+ }
25
+
26
+ asyncEnd (ctx) {
27
+ ctx.retryStepSpan?.finish()
28
+ }
29
+ }
30
+
31
+ module.exports = AwsDurableExecutionSdkJsCheckpointPlugin
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ const ClientPlugin = require('../../dd-trace/src/plugins/client')
4
+ const { addOpMeta, unwrapDurableError } = require('./util')
5
+
6
+ class AwsDurableExecutionSdkJsClientPlugin extends ClientPlugin {
7
+ static id = 'aws-durable-execution-sdk-js'
8
+ static type = 'serverless'
9
+ static prefix = 'tracing:orchestrion:@aws/durable-execution-sdk-js:DurableContextImpl_invoke'
10
+ static settleChannel = 'apm:aws-durable-execution-sdk-js:invoke:settle'
11
+
12
+ constructor (...args) {
13
+ super(...args)
14
+ this.addSub(this.constructor.settleChannel, ctx => this.settle(ctx))
15
+ }
16
+
17
+ // invoke has two overloads: invoke(name, funcId, ...) and invoke(funcId, ...).
18
+ // They're distinguished by whether args[1] is a string (named form) or not.
19
+ bindStart (ctx) {
20
+ const args = ctx.arguments || []
21
+ const isNamed = typeof args[1] === 'string'
22
+ const operationName = isNamed ? args[0] : undefined
23
+ const functionName = isNamed ? args[1] : args[0]
24
+
25
+ const meta = {}
26
+ if (functionName) {
27
+ meta['aws.durable.invoke.function_name'] = functionName
28
+ }
29
+ if (operationName) {
30
+ meta['aws.durable.operation_name'] = operationName
31
+ }
32
+ addOpMeta(meta, ctx.self)
33
+
34
+ this.startSpan(this.operationName(), {
35
+ resource: operationName,
36
+ kind: this.constructor.kind,
37
+ meta,
38
+ }, ctx)
39
+
40
+ return ctx.currentStore
41
+ }
42
+
43
+ settle (ctx) {
44
+ if (ctx.error !== undefined) {
45
+ ctx.currentStore?.span?.setTag('error', unwrapDurableError(ctx))
46
+ }
47
+ this.finish(ctx)
48
+ }
49
+
50
+ error (ctxOrError) {
51
+ this.settle(ctxOrError)
52
+ }
53
+ }
54
+
55
+ module.exports = AwsDurableExecutionSdkJsClientPlugin
@@ -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