dd-trace 5.100.0 → 5.102.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 (189) hide show
  1. package/index.d.ts +14 -0
  2. package/package.json +11 -9
  3. package/packages/datadog-instrumentations/src/aerospike.js +2 -2
  4. package/packages/datadog-instrumentations/src/ai.js +8 -8
  5. package/packages/datadog-instrumentations/src/amqplib.js +6 -7
  6. package/packages/datadog-instrumentations/src/anthropic.js +10 -10
  7. package/packages/datadog-instrumentations/src/apollo-server-core.js +3 -3
  8. package/packages/datadog-instrumentations/src/apollo-server.js +5 -5
  9. package/packages/datadog-instrumentations/src/avsc.js +6 -6
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +151 -67
  11. package/packages/datadog-instrumentations/src/azure-durable-functions.js +8 -8
  12. package/packages/datadog-instrumentations/src/bluebird.js +2 -2
  13. package/packages/datadog-instrumentations/src/body-parser.js +2 -2
  14. package/packages/datadog-instrumentations/src/cassandra-driver.js +7 -7
  15. package/packages/datadog-instrumentations/src/child_process.js +12 -12
  16. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +9 -9
  17. package/packages/datadog-instrumentations/src/connect.js +7 -7
  18. package/packages/datadog-instrumentations/src/cookie-parser.js +4 -4
  19. package/packages/datadog-instrumentations/src/cookie.js +2 -2
  20. package/packages/datadog-instrumentations/src/couchbase.js +16 -30
  21. package/packages/datadog-instrumentations/src/crypto.js +4 -4
  22. package/packages/datadog-instrumentations/src/cucumber.js +77 -16
  23. package/packages/datadog-instrumentations/src/cypress.js +5 -3
  24. package/packages/datadog-instrumentations/src/dns.js +0 -3
  25. package/packages/datadog-instrumentations/src/elasticsearch.js +8 -11
  26. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +6 -6
  27. package/packages/datadog-instrumentations/src/express-session.js +4 -4
  28. package/packages/datadog-instrumentations/src/express.js +10 -11
  29. package/packages/datadog-instrumentations/src/fastify.js +2 -2
  30. package/packages/datadog-instrumentations/src/fs.js +14 -14
  31. package/packages/datadog-instrumentations/src/google-cloud-pubsub.js +5 -7
  32. package/packages/datadog-instrumentations/src/google-genai.js +4 -4
  33. package/packages/datadog-instrumentations/src/grpc/server.js +2 -2
  34. package/packages/datadog-instrumentations/src/hapi.js +2 -2
  35. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +8 -8
  36. package/packages/datadog-instrumentations/src/helpers/promise.js +2 -2
  37. package/packages/datadog-instrumentations/src/hono.js +2 -2
  38. package/packages/datadog-instrumentations/src/http/client.js +26 -9
  39. package/packages/datadog-instrumentations/src/http/server.js +9 -9
  40. package/packages/datadog-instrumentations/src/jest.js +93 -63
  41. package/packages/datadog-instrumentations/src/kafkajs.js +9 -9
  42. package/packages/datadog-instrumentations/src/knex.js +17 -17
  43. package/packages/datadog-instrumentations/src/koa.js +12 -12
  44. package/packages/datadog-instrumentations/src/ldapjs.js +5 -5
  45. package/packages/datadog-instrumentations/src/light-my-request.js +2 -2
  46. package/packages/datadog-instrumentations/src/limitd-client.js +4 -4
  47. package/packages/datadog-instrumentations/src/lodash.js +4 -4
  48. package/packages/datadog-instrumentations/src/mariadb.js +13 -13
  49. package/packages/datadog-instrumentations/src/memcached.js +2 -2
  50. package/packages/datadog-instrumentations/src/microgateway-core.js +2 -2
  51. package/packages/datadog-instrumentations/src/mocha/common.js +7 -4
  52. package/packages/datadog-instrumentations/src/mocha/main.js +37 -14
  53. package/packages/datadog-instrumentations/src/mocha/utils.js +133 -16
  54. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -7
  55. package/packages/datadog-instrumentations/src/mongodb-core.js +9 -22
  56. package/packages/datadog-instrumentations/src/mongodb.js +5 -5
  57. package/packages/datadog-instrumentations/src/mongoose.js +21 -21
  58. package/packages/datadog-instrumentations/src/mquery.js +5 -5
  59. package/packages/datadog-instrumentations/src/multer.js +4 -4
  60. package/packages/datadog-instrumentations/src/mysql.js +16 -16
  61. package/packages/datadog-instrumentations/src/mysql2.js +4 -4
  62. package/packages/datadog-instrumentations/src/net.js +14 -8
  63. package/packages/datadog-instrumentations/src/nyc.js +5 -5
  64. package/packages/datadog-instrumentations/src/openai.js +19 -19
  65. package/packages/datadog-instrumentations/src/oracledb.js +6 -6
  66. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
  67. package/packages/datadog-instrumentations/src/passport-utils.js +5 -5
  68. package/packages/datadog-instrumentations/src/pg.js +15 -15
  69. package/packages/datadog-instrumentations/src/pino.js +6 -10
  70. package/packages/datadog-instrumentations/src/playwright.js +20 -15
  71. package/packages/datadog-instrumentations/src/protobufjs.js +16 -16
  72. package/packages/datadog-instrumentations/src/redis.js +1 -2
  73. package/packages/datadog-instrumentations/src/restify.js +2 -2
  74. package/packages/datadog-instrumentations/src/router.js +12 -12
  75. package/packages/datadog-instrumentations/src/stripe.js +12 -12
  76. package/packages/datadog-instrumentations/src/vitest.js +107 -26
  77. package/packages/datadog-instrumentations/src/winston.js +4 -4
  78. package/packages/datadog-instrumentations/src/ws.js +7 -7
  79. package/packages/datadog-plugin-aws-sdk/src/base.js +52 -4
  80. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +19 -12
  81. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +45 -35
  82. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +33 -22
  83. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +12 -13
  84. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +73 -54
  85. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +19 -17
  86. package/packages/datadog-plugin-aws-sdk/src/util.js +22 -0
  87. package/packages/datadog-plugin-bullmq/src/consumer.js +2 -2
  88. package/packages/datadog-plugin-bullmq/src/producer.js +14 -20
  89. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +6 -6
  90. package/packages/datadog-plugin-cucumber/src/index.js +4 -0
  91. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +18 -4
  92. package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
  93. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +1 -5
  94. package/packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js +3 -1
  95. package/packages/datadog-plugin-http/src/client.js +1 -5
  96. package/packages/datadog-plugin-jest/src/util.js +1 -2
  97. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
  98. package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
  99. package/packages/datadog-plugin-mocha/src/index.js +4 -0
  100. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -1
  101. package/packages/datadog-plugin-openai/src/tracing.js +12 -23
  102. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  103. package/packages/datadog-plugin-vitest/src/index.js +8 -1
  104. package/packages/datadog-shimmer/src/shimmer.js +7 -1
  105. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-password-rules.js +1 -1
  106. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secret-rules.js +81 -81
  107. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
  108. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +2 -2
  109. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +2 -2
  110. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -2
  111. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -0
  112. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -3
  113. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +83 -48
  114. package/packages/dd-trace/src/appsec/index.js +21 -24
  115. package/packages/dd-trace/src/appsec/reporter.js +7 -2
  116. package/packages/dd-trace/src/appsec/rule_manager.js +4 -2
  117. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +31 -16
  118. package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
  119. package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
  120. package/packages/dd-trace/src/config/config-types.d.ts +0 -2
  121. package/packages/dd-trace/src/config/git_properties.js +2 -2
  122. package/packages/dd-trace/src/config/index.js +1 -55
  123. package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
  124. package/packages/dd-trace/src/datastreams/encoding.js +39 -28
  125. package/packages/dd-trace/src/datastreams/index.js +2 -1
  126. package/packages/dd-trace/src/datastreams/pathway.js +29 -26
  127. package/packages/dd-trace/src/datastreams/processor.js +18 -17
  128. package/packages/dd-trace/src/datastreams/size.js +6 -2
  129. package/packages/dd-trace/src/debugger/config.js +5 -2
  130. package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
  131. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
  132. package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +1 -0
  133. package/packages/dd-trace/src/dogstatsd.js +10 -7
  134. package/packages/dd-trace/src/encode/0.4.js +759 -234
  135. package/packages/dd-trace/src/encode/0.5.js +15 -9
  136. package/packages/dd-trace/src/encode/agentless-json.js +2 -2
  137. package/packages/dd-trace/src/encode/tags-processors.js +2 -27
  138. package/packages/dd-trace/src/exporters/common/request.js +22 -11
  139. package/packages/dd-trace/src/exporters/common/retry.js +104 -0
  140. package/packages/dd-trace/src/git_metadata.js +66 -0
  141. package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
  142. package/packages/dd-trace/src/id.js +15 -26
  143. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  144. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -2
  145. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
  146. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
  147. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +33 -13
  148. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
  149. package/packages/dd-trace/src/llmobs/sdk.js +29 -27
  150. package/packages/dd-trace/src/llmobs/span_processor.js +52 -6
  151. package/packages/dd-trace/src/llmobs/tagger.js +42 -0
  152. package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
  153. package/packages/dd-trace/src/llmobs/util.js +81 -5
  154. package/packages/dd-trace/src/msgpack/chunk.js +6 -3
  155. package/packages/dd-trace/src/openfeature/noop.js +40 -36
  156. package/packages/dd-trace/src/openfeature/writers/exposures.js +33 -52
  157. package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
  158. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
  159. package/packages/dd-trace/src/opentelemetry/context_manager.js +11 -2
  160. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +1 -2
  161. package/packages/dd-trace/src/opentelemetry/span-helpers.js +188 -50
  162. package/packages/dd-trace/src/opentelemetry/span.js +42 -80
  163. package/packages/dd-trace/src/opentelemetry/tracer.js +0 -22
  164. package/packages/dd-trace/src/opentracing/propagation/text_map.js +65 -27
  165. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +2 -11
  166. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +58 -22
  167. package/packages/dd-trace/src/opentracing/span.js +56 -48
  168. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  169. package/packages/dd-trace/src/plugins/util/ci.js +1 -1
  170. package/packages/dd-trace/src/plugins/util/git-cache.js +3 -5
  171. package/packages/dd-trace/src/plugins/util/test.js +19 -7
  172. package/packages/dd-trace/src/plugins/util/url.js +1 -3
  173. package/packages/dd-trace/src/plugins/util/user-provided-git.js +1 -1
  174. package/packages/dd-trace/src/plugins/util/web.js +5 -7
  175. package/packages/dd-trace/src/priority_sampler.js +6 -4
  176. package/packages/dd-trace/src/profiling/config.js +5 -4
  177. package/packages/dd-trace/src/profiling/profilers/events.js +3 -23
  178. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -5
  179. package/packages/dd-trace/src/remote_config/index.js +5 -3
  180. package/packages/dd-trace/src/runtime_metrics/index.js +2 -2
  181. package/packages/dd-trace/src/scope.js +3 -10
  182. package/packages/dd-trace/src/serverless.js +1 -4
  183. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +7 -1
  184. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +4 -0
  185. package/packages/dd-trace/src/span_format.js +52 -5
  186. package/packages/dd-trace/src/span_processor.js +0 -4
  187. package/packages/dd-trace/src/spanleak.js +0 -1
  188. package/packages/dd-trace/src/tracer.js +7 -7
  189. package/packages/dd-trace/src/util.js +17 -0
@@ -8,10 +8,51 @@ const { tagsFromRequest, tagsFromResponse } = require('../../dd-trace/src/payloa
8
8
  const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
9
9
  const { IS_SERVERLESS } = require('../../dd-trace/src/serverless')
10
10
 
11
+ const RESPONSE_SKIP_KEYS = new Set(['request', 'requestId', 'error', '$metadata'])
12
+
11
13
  class BaseAwsSdkPlugin extends ClientPlugin {
12
14
  static id = 'aws'
13
15
  static isPayloadReporter = false
14
16
 
17
+ /**
18
+ * Append `"<key>": <JSON.stringify(value)>` to a JSON-encoded object
19
+ * payload without re-parsing when possible.
20
+ *
21
+ * Fast path: `payload` is `{}` (returns `{"<key>":<json>}`) or ends with
22
+ * `}` preceded by a non-whitespace, non-`{` byte and does not contain
23
+ * `"<key>"` anywhere. The new field is spliced in before the trailing
24
+ * brace.
25
+ *
26
+ * Slow path falls back to `JSON.parse` + assign + `JSON.stringify` so the
27
+ * result still matches the previous round-trip when the payload has
28
+ * whitespace before the trailing `}`, is not a JSON object, or already
29
+ * contains `key`. The slow path replaces an existing `key` rather than
30
+ * merging — callers that need to preserve nested fields under `key` must
31
+ * read and merge before calling.
32
+ *
33
+ * @param {string} payload
34
+ * @param {string} key Top-level key to insert. Must be a simple
35
+ * identifier that does not need JSON escaping.
36
+ * @param {object} value Value to inject; will be `JSON.stringify`'d.
37
+ * @returns {string}
38
+ */
39
+ static injectFieldIntoJsonObject (payload, key, value) {
40
+ const last = payload.length - 1
41
+ if (last >= 1 && payload[last] === '}') {
42
+ if (last === 1) {
43
+ return `{"${key}":${JSON.stringify(value)}}`
44
+ }
45
+ const before = payload.charCodeAt(last - 1)
46
+ const isWhitespace = before === 0x20 || before === 0x09 || before === 0x0A || before === 0x0D
47
+ if (!isWhitespace && before !== 0x7B && !payload.includes(`"${key}"`)) {
48
+ return `${payload.slice(0, last)},"${key}":${JSON.stringify(value)}}`
49
+ }
50
+ }
51
+ const obj = JSON.parse(payload)
52
+ obj[key] = value
53
+ return JSON.stringify(obj)
54
+ }
55
+
15
56
  get serviceIdentifier () {
16
57
  const id = this.constructor.id.toLowerCase()
17
58
  Object.defineProperty(this, 'serviceIdentifier', {
@@ -224,12 +265,19 @@ class BaseAwsSdkPlugin extends ClientPlugin {
224
265
  }
225
266
 
226
267
  extractResponseBody (response) {
227
- if (response.hasOwnProperty('data')) {
268
+ if (Object.hasOwn(response, 'data')) {
228
269
  return response.data
229
270
  }
230
- return Object.fromEntries(
231
- Object.entries(response).filter(([key]) => !['request', 'requestId', 'error', '$metadata'].includes(key))
232
- )
271
+ // `{ ...response }` followed by `delete body.X` allocates a copy and then
272
+ // pushes the copy into V8 dictionary mode for every SDK response. Filter
273
+ // on build instead -- ~2.3x faster on the typical 4-of-8-keys shape.
274
+ const body = {}
275
+ for (const key of Object.keys(response)) {
276
+ if (!RESPONSE_SKIP_KEYS.has(key)) {
277
+ body[key] = response[key]
278
+ }
279
+ }
280
+ return body
233
281
  }
234
282
 
235
283
  generateTags () {
@@ -34,20 +34,27 @@ class EventBridge extends BaseAwsSdkPlugin {
34
34
  request.params.Entries &&
35
35
  request.params.Entries.length > 0 &&
36
36
  request.params.Entries[0].Detail) {
37
+ const injected = {}
38
+ this.tracer.inject(span, 'text_map', injected)
39
+
40
+ // Only `injectFieldIntoJsonObject` can throw (the slow path
41
+ // `JSON.parse` for non-`{...}` payloads). Tighten the catch around
42
+ // it so the rest of the body stays in V8's optimisable surface.
43
+ let finalData
37
44
  try {
38
- const details = JSON.parse(request.params.Entries[0].Detail)
39
- details._datadog = {}
40
- this.tracer.inject(span, 'text_map', details._datadog)
41
- const finalData = JSON.stringify(details)
42
- const byteSize = Buffer.byteLength(finalData)
43
- if (byteSize >= (1024 * 256)) {
44
- log.info('Payload size too large to pass context')
45
- return
46
- }
47
- request.params.Entries[0].Detail = finalData
48
- } catch (e) {
49
- log.error('EventBridge error injecting request', e)
45
+ finalData = BaseAwsSdkPlugin.injectFieldIntoJsonObject(
46
+ request.params.Entries[0].Detail, '_datadog', injected
47
+ )
48
+ } catch (error) {
49
+ log.error('EventBridge error injecting request', error)
50
+ return
51
+ }
52
+
53
+ if (Buffer.byteLength(finalData) >= 1024 * 256) {
54
+ log.info('Payload size too large to pass context')
55
+ return
50
56
  }
57
+ request.params.Entries[0].Detail = finalData
51
58
  }
52
59
  }
53
60
  }
@@ -2,6 +2,11 @@
2
2
  const { DsmPathwayCodec, getSizeOrZero } = require('../../../dd-trace/src/datastreams')
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
+ const { isEmpty } = require('../util')
6
+
7
+ function recordDataAsString (data) {
8
+ return Buffer.isBuffer(data) ? data.toString('utf8') : Buffer.from(data).toString('utf8')
9
+ }
5
10
 
6
11
  class Kinesis extends BaseAwsSdkPlugin {
7
12
  static id = 'kinesis'
@@ -90,14 +95,14 @@ class Kinesis extends BaseAwsSdkPlugin {
90
95
  const record = response.Records[0]
91
96
 
92
97
  try {
93
- const decodedData = JSON.parse(Buffer.from(record.Data).toString())
98
+ const decodedData = JSON.parse(recordDataAsString(record.Data))
94
99
 
95
100
  return {
96
101
  maybeChildOf: this.tracer.extract('text_map', decodedData._datadog),
97
102
  parsedAttributes: decodedData._datadog,
98
103
  }
99
- } catch (e) {
100
- log.error('Kinesis error extracting response', e)
104
+ } catch (error) {
105
+ log.error('Kinesis error extracting response', error)
101
106
  }
102
107
  }
103
108
 
@@ -107,27 +112,32 @@ class Kinesis extends BaseAwsSdkPlugin {
107
112
  if (operation !== 'getRecords') return
108
113
  if (!response || !response.Records || !response.Records[0]) return
109
114
 
110
- // we only want to set the payloadSize on the span if we have one message, not repeatedly
115
+ // Only attribute payloadSize to the span when there is a single record.
111
116
  span = response.Records.length > 1 ? null : span
112
117
 
118
+ const tags = streamName
119
+ ? ['direction:in', `topic:${streamName}`, 'type:kinesis']
120
+ : ['direction:in', 'type:kinesis']
121
+
113
122
  for (const record of response.Records) {
114
- const parsedAttributes = JSON.parse(Buffer.from(record.Data).toString())
123
+ let parsedAttributes
124
+ try {
125
+ parsedAttributes = JSON.parse(recordDataAsString(record.Data))
126
+ } catch {
127
+ // Non-JSON record. Skip DSM context for this entry; the
128
+ // checkpoint payload size below is still reported.
129
+ }
115
130
 
116
131
  const payloadSize = getSizeOrZero(record.Data)
117
132
  if (parsedAttributes?._datadog) {
118
133
  this.tracer.decodeDataStreamsContext(parsedAttributes._datadog)
119
134
  }
120
- const tags = streamName
121
- ? ['direction:in', `topic:${streamName}`, 'type:kinesis']
122
- : ['direction:in', 'type:kinesis']
123
- this.tracer
124
- .setCheckpoint(tags, span, payloadSize)
135
+ this.tracer.setCheckpoint(tags, span, payloadSize)
125
136
  }
126
137
  }
127
138
 
128
- // AWS-SDK will b64 kinesis payloads
129
- // or will accept an already b64 encoded payload
130
- // This method handles both
139
+ // AWS-SDK base64-encodes kinesis payloads but also accepts an already
140
+ // base64-encoded payload; both shapes land here.
131
141
  _tryParse (body) {
132
142
  try {
133
143
  return JSON.parse(body)
@@ -135,7 +145,7 @@ class Kinesis extends BaseAwsSdkPlugin {
135
145
  log.info('Not JSON string. Trying Base64 encoded JSON string')
136
146
  }
137
147
  try {
138
- return JSON.parse(Buffer.from(body, 'base64').toString('ascii'), true)
148
+ return JSON.parse(Buffer.from(body, 'base64').toString('ascii'))
139
149
  } catch {
140
150
  return null
141
151
  }
@@ -179,36 +189,36 @@ class Kinesis extends BaseAwsSdkPlugin {
179
189
  }
180
190
 
181
191
  const ddInfo = {}
182
- // for now, we only want to inject to the first message, this may change for batches in the future
183
- if (injectTraceContext) { this.tracer.inject(span, 'text_map', ddInfo) }
192
+ // For now we only inject to the first message; batches may change later.
193
+ if (injectTraceContext) {
194
+ this.tracer.inject(span, 'text_map', ddInfo)
195
+ }
184
196
 
185
- // set DSM hash if enabled
186
197
  if (this.config.dsmEnabled) {
187
198
  parsedData._datadog = ddInfo
188
- const dataStreamsContext = this.setDSMCheckpoint(span, parsedData, stream)
189
- DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
199
+ const dataStreamsContext = this.setDSMCheckpoint(span, params, stream)
200
+ if (dataStreamsContext) {
201
+ DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
202
+ }
190
203
  }
191
204
 
192
- if (Object.keys(ddInfo).length !== 0) {
193
- parsedData._datadog = ddInfo
194
- const finalData = Buffer.from(JSON.stringify(parsedData))
195
- const byteSize = finalData.length
196
- // Kinesis max payload size is 1MB
197
- // So we must ensure adding DD context won't go over that (512b is an estimate)
198
- if (byteSize >= 1_048_576) {
199
- log.info('Payload size too large to pass context')
200
- return
201
- }
202
- params.Data = finalData
205
+ if (isEmpty(ddInfo)) return
206
+
207
+ parsedData._datadog = ddInfo
208
+ const serialized = JSON.stringify(parsedData)
209
+ const byteSize = Buffer.byteLength(serialized, 'utf8')
210
+ // Kinesis max payload size is 1 MiB; bail if our context push tipped us over.
211
+ if (byteSize >= 1_048_576) {
212
+ log.info('Payload size too large to pass context')
213
+ return
203
214
  }
215
+ params.Data = Buffer.from(serialized, 'utf8')
204
216
  }
205
217
 
206
- setDSMCheckpoint (span, parsedData, stream) {
207
- // get payload size of request data
208
- const payloadSize = Buffer.byteLength(JSON.stringify(parsedData))
209
- const dataStreamsContext = this.tracer
218
+ setDSMCheckpoint (span, params, stream) {
219
+ const payloadSize = getSizeOrZero(params.Data)
220
+ return this.tracer
210
221
  .setCheckpoint(['direction:out', `topic:${stream}`, 'type:kinesis'], span, payloadSize)
211
- return dataStreamsContext
212
222
  }
213
223
  }
214
224
 
@@ -18,33 +18,44 @@ class Lambda extends BaseAwsSdkPlugin {
18
18
 
19
19
  requestInject (span, request) {
20
20
  const operation = request.operation
21
- if (operation === 'invoke') {
22
- if (!request.params) {
23
- request.params = {}
24
- }
21
+ if (operation !== 'invoke') return
22
+
23
+ if (!request.params) {
24
+ request.params = {}
25
+ }
26
+
27
+ const isSyncInvocation = !request.params.InvocationType ||
28
+ request.params.InvocationType === 'RequestResponse'
29
+ if (!isSyncInvocation) return
30
+
31
+ const injected = {}
32
+ this.tracer.inject(span, 'text_map', injected)
25
33
 
26
- const isSyncInvocation = !request.params.InvocationType ||
27
- request.params.InvocationType === 'RequestResponse'
34
+ let newContextJson
35
+ if (request.params.ClientContext) {
36
+ const clientContextJson = Buffer.from(request.params.ClientContext, 'base64').toString('utf8')
28
37
 
29
- if (isSyncInvocation) {
30
- try {
31
- // Check to see if there's already a config on the request
32
- let clientContext = {}
33
- if (request.params.ClientContext) {
34
- const clientContextJson = Buffer.from(request.params.ClientContext, 'base64').toString('utf8')
35
- clientContext = JSON.parse(clientContextJson)
36
- }
37
- if (!clientContext.custom) {
38
- clientContext.custom = {}
39
- }
40
- this.tracer.inject(span, 'text_map', clientContext.custom)
41
- const newContextBase64 = Buffer.from(JSON.stringify(clientContext)).toString('base64')
42
- request.params.ClientContext = newContextBase64
43
- } catch (err) {
44
- log.error('Lambda error injecting request', err)
38
+ // The two throwing surfaces here are the inline `JSON.parse` and the
39
+ // slow path inside `injectFieldIntoJsonObject`. Tighten the catch
40
+ // around the JSON ops so the rest of the inject stays optimisable.
41
+ try {
42
+ if (clientContextJson.includes('"custom"')) {
43
+ // Existing customer keys under `custom` survive the round-trip.
44
+ const clientContext = JSON.parse(clientContextJson)
45
+ if (!clientContext.custom) clientContext.custom = {}
46
+ Object.assign(clientContext.custom, injected)
47
+ newContextJson = JSON.stringify(clientContext)
48
+ } else {
49
+ newContextJson = BaseAwsSdkPlugin.injectFieldIntoJsonObject(clientContextJson, 'custom', injected)
45
50
  }
51
+ } catch (error) {
52
+ log.error('Lambda error injecting request', error)
53
+ return
46
54
  }
55
+ } else {
56
+ newContextJson = `{"custom":${JSON.stringify(injected)}}`
47
57
  }
58
+ request.params.ClientContext = Buffer.from(newContextJson).toString('base64')
48
59
  }
49
60
 
50
61
  operationFromRequest (request) {
@@ -2,6 +2,7 @@
2
2
  const { DsmPathwayCodec, getHeadersSize } = require('../../../dd-trace/src/datastreams')
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
+ const { isEmpty } = require('../util')
5
6
 
6
7
  class Sns extends BaseAwsSdkPlugin {
7
8
  static id = 'sns'
@@ -14,12 +15,10 @@ class Sns extends BaseAwsSdkPlugin {
14
15
  if (!params.TopicArn && !(response.data && response.data.TopicArn)) return {}
15
16
  const TopicArn = params.TopicArn || response.data.TopicArn
16
17
 
17
- // Split the ARN into its parts
18
- // ex.'arn:aws:sns:us-east-1:123456789012:my-topic'
19
- const arnParts = TopicArn.split(':')
20
-
21
- // Get the topic name from the last part of the ARN
22
- const topicName = arnParts.at(-1)
18
+ // Get the topic name from the last `:`-delimited segment of the ARN
19
+ // (e.g. 'my-topic' in 'arn:aws:sns:us-east-1:123456789012:my-topic')
20
+ // without allocating an intermediate parts array.
21
+ const topicName = TopicArn.slice(TopicArn.lastIndexOf(':') + 1)
23
22
 
24
23
  return {
25
24
  'resource.name': `${operation} ${params.TopicArn || response.data.TopicArn}`,
@@ -103,12 +102,14 @@ class Sns extends BaseAwsSdkPlugin {
103
102
  DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
104
103
  }
105
104
 
106
- if (Object.keys(ddInfo).length !== 0) {
105
+ if (isEmpty(ddInfo)) {
106
+ if (params.MessageAttributes._datadog) {
107
+ // let's avoid adding any additional information to payload if we failed to inject
108
+ delete params.MessageAttributes._datadog
109
+ }
110
+ } else {
107
111
  // BINARY types are automatically base64 encoded
108
112
  params.MessageAttributes._datadog.BinaryValue = Buffer.from(JSON.stringify(ddInfo))
109
- } else if (params.MessageAttributes._datadog) {
110
- // let's avoid adding any additional information to payload if we failed to inject
111
- delete params.MessageAttributes._datadog
112
113
  }
113
114
  }
114
115
 
@@ -116,9 +117,7 @@ class Sns extends BaseAwsSdkPlugin {
116
117
  // only set a checkpoint if publishing to a topic
117
118
  if (topicArn) {
118
119
  const payloadSize = getHeadersSize(params)
119
- const dataStreamsContext = this.tracer
120
- .setCheckpoint(['direction:out', `topic:${topicArn}`, 'type:sns'], span, payloadSize)
121
- return dataStreamsContext
120
+ return this.tracer.setCheckpoint(['direction:out', `topic:${topicArn}`, 'type:sns'], span, payloadSize)
122
121
  }
123
122
  }
124
123
  }
@@ -3,7 +3,7 @@
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
5
  const { DsmPathwayCodec, getHeadersSize } = require('../../../dd-trace/src/datastreams')
6
- const { extractQueueMetadata } = require('../util')
6
+ const { extractQueueMetadata, isEmpty } = require('../util')
7
7
 
8
8
  class Sqs extends BaseAwsSdkPlugin {
9
9
  static id = 'sqs'
@@ -23,25 +23,32 @@ class Sqs extends BaseAwsSdkPlugin {
23
23
 
24
24
  let store = this._parentMap.get(request)
25
25
  let span
26
- let parsedMessageAttributes = null
27
- if (contextExtraction && contextExtraction.datadogContext) {
28
- ctx.needsFinish = true
29
- const options = {
30
- childOf: contextExtraction.datadogContext,
31
- meta: {
32
- ...this.requestTags.get(request),
33
- 'span.kind': 'server',
34
- },
35
- integrationName: 'aws-sdk',
26
+ let parsedMessageAttributes
27
+ let parsedFirstBody
28
+ let firstBodyChecked = false
29
+ if (contextExtraction !== undefined) {
30
+ parsedFirstBody = contextExtraction.parsedBody
31
+ firstBodyChecked = contextExtraction.bodyChecked === true
32
+ if (contextExtraction.datadogContext !== undefined) {
33
+ ctx.needsFinish = true
34
+ const options = {
35
+ childOf: contextExtraction.datadogContext,
36
+ meta: {
37
+ ...this.requestTags.get(request),
38
+ 'span.kind': 'server',
39
+ },
40
+ integrationName: 'aws-sdk',
41
+ }
42
+ parsedMessageAttributes = contextExtraction.parsedAttributes
43
+ span = this.startSpan('aws.response', options, ctx)
44
+ store = ctx.currentStore
36
45
  }
37
- parsedMessageAttributes = contextExtraction.parsedAttributes
38
- span = this.startSpan('aws.response', options, ctx)
39
- store = ctx.currentStore
40
46
  }
41
47
 
42
- // extract DSM context after as we might not have a parent-child but may have a DSM context
48
+ // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
43
49
  this.responseExtractDSMContext(
44
- request.operation, request.params, response, span || null, { parsedAttributes: parsedMessageAttributes }
50
+ request.operation, request.params, response, span ?? null,
51
+ { parsedAttributes: parsedMessageAttributes, parsedFirstBody, firstBodyChecked }
45
52
  )
46
53
 
47
54
  return store
@@ -128,21 +135,23 @@ class Sqs extends BaseAwsSdkPlugin {
128
135
  if (!response || !response.Messages || !response.Messages[0]) return
129
136
 
130
137
  let message = response.Messages[0]
138
+ let parsedBody
131
139
 
132
140
  if (message.Body) {
133
141
  try {
134
- const body = JSON.parse(message.Body)
135
-
136
- // SNS to SQS
137
- if (body.Type === 'Notification') {
138
- message = body
139
- }
142
+ parsedBody = JSON.parse(message.Body)
140
143
  } catch {
141
144
  // SQS to SQS
142
145
  }
146
+ // SNS to SQS
147
+ if (parsedBody?.Type === 'Notification') {
148
+ message = parsedBody
149
+ }
143
150
  }
144
151
 
145
- if (!message.MessageAttributes || !message.MessageAttributes._datadog) return
152
+ if (!message.MessageAttributes || !message.MessageAttributes._datadog) {
153
+ return { parsedBody, bodyChecked: true }
154
+ }
146
155
 
147
156
  const datadogAttribute = message.MessageAttributes._datadog
148
157
 
@@ -151,8 +160,12 @@ class Sqs extends BaseAwsSdkPlugin {
151
160
  return {
152
161
  datadogContext: this.tracer.extract('text_map', parsedAttributes),
153
162
  parsedAttributes,
163
+ parsedBody,
164
+ bodyChecked: true,
154
165
  }
155
166
  }
167
+
168
+ return { parsedBody, bodyChecked: true }
156
169
  }
157
170
 
158
171
  parseDatadogAttributes (attributes) {
@@ -164,36 +177,43 @@ class Sqs extends BaseAwsSdkPlugin {
164
177
  const buffer = Buffer.from(attributes.Value ?? attributes.BinaryValue, 'base64')
165
178
  return JSON.parse(buffer)
166
179
  }
167
- } catch (e) {
168
- log.error('Sqs error parsing DD attributes', e)
180
+ } catch (error) {
181
+ log.error('Sqs error parsing DD attributes', error)
169
182
  }
170
183
  }
171
184
 
172
185
  responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
173
186
  let { parsedAttributes } = kwargs
187
+ const { parsedFirstBody, firstBodyChecked } = kwargs
174
188
  if (!this.config.dsmEnabled) return
175
189
  if (operation !== 'receiveMessage') return
176
190
  if (!response || !response.Messages || !response.Messages[0]) return
177
191
 
178
- // we only want to set the payloadSize on the span if we have one message
192
+ // Only attribute payloadSize to the span when there is a single message.
179
193
  span = response.Messages.length > 1 ? null : span
180
194
 
181
- for (let message of response.Messages) {
182
- // we may have already parsed the message attributes when extracting trace context
195
+ // QueueUrl is the same for the whole receive batch.
196
+ const queue = params.QueueUrl.slice(params.QueueUrl.lastIndexOf('/') + 1)
197
+
198
+ for (let i = 0; i < response.Messages.length; i++) {
199
+ let message = response.Messages[i]
183
200
  if (!parsedAttributes) {
184
- if (message.Body) {
201
+ let body
202
+ // responseExtract already parsed message[0]; reuse that result instead of re-parsing.
203
+ if (i === 0 && firstBodyChecked) {
204
+ body = parsedFirstBody
205
+ } else if (message.Body) {
185
206
  try {
186
- const body = JSON.parse(message.Body)
187
-
188
- // SNS to SQS
189
- if (body.Type === 'Notification') {
190
- message = body
191
- }
207
+ body = JSON.parse(message.Body)
192
208
  } catch {
193
209
  // SQS to SQS
194
210
  }
195
211
  }
196
- if (!parsedAttributes && message.MessageAttributes && message.MessageAttributes._datadog) {
212
+ // SNS to SQS
213
+ if (body?.Type === 'Notification') {
214
+ message = body
215
+ }
216
+ if (message.MessageAttributes && message.MessageAttributes._datadog) {
197
217
  parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
198
218
  }
199
219
  }
@@ -201,7 +221,6 @@ class Sqs extends BaseAwsSdkPlugin {
201
221
  Body: message.Body,
202
222
  MessageAttributes: message.MessageAttributes,
203
223
  })
204
- const queue = params.QueueUrl.split('/').pop()
205
224
  if (parsedAttributes) {
206
225
  this.tracer.decodeDataStreamsContext(parsedAttributes)
207
226
  }
@@ -253,34 +272,35 @@ class Sqs extends BaseAwsSdkPlugin {
253
272
  // TODO: add test when the test suite is fixed
254
273
  return
255
274
  }
275
+
256
276
  const ddInfo = {}
257
- // for now, we only want to inject to the first message, this may change for batches in the future
277
+ // For now we only inject to the first message; batches may change later.
258
278
  if (injectTraceContext) {
259
279
  this.tracer.inject(span, 'text_map', ddInfo)
260
- params.MessageAttributes._datadog = {
261
- DataType: 'String',
262
- StringValue: JSON.stringify(ddInfo),
263
- }
264
280
  }
265
281
 
266
282
  if (this.config.dsmEnabled) {
267
- if (!params.MessageAttributes._datadog) {
268
- params.MessageAttributes._datadog = {
269
- DataType: 'String',
270
- StringValue: JSON.stringify(ddInfo),
271
- }
283
+ // Attach `_datadog` before measuring so the DSM payload size metric
284
+ // matches the on-wire payload, then update with the encoded context.
285
+ params.MessageAttributes._datadog = {
286
+ DataType: 'String',
287
+ StringValue: JSON.stringify(ddInfo),
272
288
  }
273
-
274
289
  const dataStreamsContext = this.setDSMCheckpoint(span, params, queueUrl)
275
290
  if (dataStreamsContext) {
276
291
  DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
277
292
  params.MessageAttributes._datadog.StringValue = JSON.stringify(ddInfo)
293
+ } else if (isEmpty(ddInfo)) {
294
+ delete params.MessageAttributes._datadog
278
295
  }
296
+ return
279
297
  }
280
298
 
281
- if (params.MessageAttributes._datadog && Object.keys(ddInfo).length === 0) {
282
- // let's avoid adding any additional information to payload if we failed to inject
283
- delete params.MessageAttributes._datadog
299
+ if (isEmpty(ddInfo)) return
300
+
301
+ params.MessageAttributes._datadog = {
302
+ DataType: 'String',
303
+ StringValue: JSON.stringify(ddInfo),
284
304
  }
285
305
  }
286
306
 
@@ -289,10 +309,9 @@ class Sqs extends BaseAwsSdkPlugin {
289
309
  Body: params.MessageBody,
290
310
  MessageAttributes: params.MessageAttributes,
291
311
  })
292
- const queue = queueUrl.split('/').pop()
293
- const dataStreamsContext = this.tracer
312
+ const queue = queueUrl.slice(queueUrl.lastIndexOf('/') + 1)
313
+ return this.tracer
294
314
  .setCheckpoint(['direction:out', `topic:${queue}`, 'type:sqs'], span, payloadSize)
295
- return dataStreamsContext
296
315
  }
297
316
  }
298
317
 
@@ -38,25 +38,27 @@ class Stepfunctions extends BaseAwsSdkPlugin {
38
38
 
39
39
  requestInject (span, request) {
40
40
  const operation = request.operation
41
- if (operation === 'startExecution' || operation === 'startSyncExecution') {
42
- if (!request.params || !request.params.input) {
43
- return
44
- }
41
+ if ((operation !== 'startExecution' && operation !== 'startSyncExecution') || !request.params?.input) return
42
+
43
+ const input = request.params.input
44
+ if (typeof input !== 'string' || input.length < 2) return
45
+
46
+ // Skip non-object payloads up front to avoid a `JSON.parse` round-trip.
47
+ // `trimEnd` is the identity on payloads with no trailing whitespace;
48
+ // for the rare whitespace-suffixed object the slow path inside
49
+ // `injectFieldIntoJsonObject` handles the parse + restringify.
50
+ const trimmed = input.trimEnd()
51
+ if (trimmed.length < 2 || trimmed.charCodeAt(trimmed.length - 1) !== 0x7D) return
45
52
 
46
- const input = request.params.input
53
+ const injected = {}
54
+ this.tracer.inject(span, 'text_map', injected)
47
55
 
48
- try {
49
- const inputObj = JSON.parse(input)
50
- if (inputObj !== null && typeof inputObj === 'object') {
51
- // We've parsed the input JSON string
52
- inputObj._datadog = {}
53
- this.tracer.inject(span, 'text_map', inputObj._datadog)
54
- const newInput = JSON.stringify(inputObj)
55
- request.params.input = newInput
56
- }
57
- } catch {
58
- log.info('Unable to treat input as JSON')
59
- }
56
+ // `injectFieldIntoJsonObject` is the only throwing call path
57
+ // (`JSON.parse` slow path for non-trivial JSON shapes).
58
+ try {
59
+ request.params.input = BaseAwsSdkPlugin.injectFieldIntoJsonObject(input, '_datadog', injected)
60
+ } catch {
61
+ log.info('Unable to treat input as JSON')
60
62
  }
61
63
  }
62
64
  }