dd-trace 4.28.0 → 4.30.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/CONTRIBUTING.md +98 -0
  2. package/README.md +8 -99
  3. package/ci/cypress/after-run.js +1 -0
  4. package/ci/cypress/after-spec.js +1 -0
  5. package/index.d.ts +1499 -1486
  6. package/package.json +3 -3
  7. package/packages/datadog-core/src/utils/src/get.js +11 -0
  8. package/packages/datadog-core/src/utils/src/has.js +14 -0
  9. package/packages/datadog-core/src/utils/src/set.js +16 -0
  10. package/packages/datadog-instrumentations/src/amqplib.js +1 -1
  11. package/packages/datadog-instrumentations/src/cucumber.js +157 -42
  12. package/packages/datadog-instrumentations/src/grpc/server.js +3 -1
  13. package/packages/datadog-instrumentations/src/jest.js +80 -40
  14. package/packages/datadog-instrumentations/src/mocha.js +4 -1
  15. package/packages/datadog-instrumentations/src/mongodb-core.js +34 -3
  16. package/packages/datadog-instrumentations/src/playwright.js +78 -16
  17. package/packages/datadog-plugin-amqplib/src/consumer.js +8 -4
  18. package/packages/datadog-plugin-amqplib/src/producer.js +3 -4
  19. package/packages/datadog-plugin-aws-sdk/src/base.js +3 -2
  20. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +60 -57
  21. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +42 -22
  22. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +64 -30
  23. package/packages/datadog-plugin-cucumber/src/index.js +25 -9
  24. package/packages/datadog-plugin-cypress/src/after-run.js +3 -0
  25. package/packages/datadog-plugin-cypress/src/after-spec.js +3 -0
  26. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +625 -0
  27. package/packages/datadog-plugin-cypress/src/plugin.js +6 -549
  28. package/packages/datadog-plugin-cypress/src/support.js +50 -3
  29. package/packages/datadog-plugin-graphql/src/index.js +1 -1
  30. package/packages/datadog-plugin-graphql/src/resolve.js +10 -8
  31. package/packages/datadog-plugin-grpc/src/util.js +1 -1
  32. package/packages/datadog-plugin-jest/src/index.js +11 -2
  33. package/packages/datadog-plugin-kafkajs/src/consumer.js +4 -3
  34. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -5
  35. package/packages/datadog-plugin-playwright/src/index.js +34 -3
  36. package/packages/datadog-plugin-rhea/src/consumer.js +8 -3
  37. package/packages/datadog-plugin-rhea/src/producer.js +3 -4
  38. package/packages/dd-trace/src/appsec/iast/index.js +10 -0
  39. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +18 -5
  40. package/packages/dd-trace/src/appsec/recommended.json +67 -27
  41. package/packages/dd-trace/src/appsec/remote_config/index.js +1 -1
  42. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -3
  43. package/packages/dd-trace/src/config.js +451 -459
  44. package/packages/dd-trace/src/data_streams_context.js +1 -1
  45. package/packages/dd-trace/src/datastreams/pathway.js +58 -1
  46. package/packages/dd-trace/src/datastreams/processor.js +3 -5
  47. package/packages/dd-trace/src/format.js +0 -1
  48. package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -2
  49. package/packages/dd-trace/src/opentracing/span.js +4 -4
  50. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  51. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  52. package/packages/dd-trace/src/profiling/exporters/agent.js +77 -32
  53. package/packages/dd-trace/src/telemetry/index.js +22 -34
  54. package/packages/dd-trace/src/tracer.js +3 -3
  55. package/register.js +4 -0
  56. /package/packages/{utils → datadog-core/src/utils}/src/kebabcase.js +0 -0
  57. /package/packages/{utils → datadog-core/src/utils}/src/pick.js +0 -0
  58. /package/packages/{utils → datadog-core/src/utils}/src/uniq.js +0 -0
@@ -14,6 +14,7 @@ const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
14
14
  const testToAr = new WeakMap()
15
15
  const testSuiteToAr = new Map()
16
16
  const testSuiteToTestStatuses = new Map()
17
+ const testSuiteToErrors = new Map()
17
18
 
18
19
  let startedSuites = []
19
20
 
@@ -81,7 +82,12 @@ function getRootDir (playwrightRunner) {
81
82
 
82
83
  function getProjectsFromRunner (runner) {
83
84
  const config = getPlaywrightConfig(runner)
84
- return config.projects?.map(({ project }) => project)
85
+ return config.projects?.map((project) => {
86
+ if (project.project) {
87
+ return project.project
88
+ }
89
+ return project
90
+ })
85
91
  }
86
92
 
87
93
  function getProjectsFromDispatcher (dispatcher) {
@@ -93,13 +99,55 @@ function getProjectsFromDispatcher (dispatcher) {
93
99
  return dispatcher._loader?.fullConfig()?.projects
94
100
  }
95
101
 
96
- function getBrowserNameFromProjects (projects, projectId) {
97
- if (!projects) {
102
+ function getBrowserNameFromProjects (projects, test) {
103
+ if (!projects || !test) {
98
104
  return null
99
105
  }
100
- return projects.find(project =>
101
- project.__projectId === projectId || project._id === projectId
102
- )?.name
106
+ const { _projectIndex, _projectId: testProjectId } = test
107
+
108
+ if (_projectIndex !== undefined) {
109
+ return projects[_projectIndex]?.name
110
+ }
111
+
112
+ return projects.find(({ __projectId, _id, name }) => {
113
+ if (__projectId !== undefined) {
114
+ return __projectId === testProjectId
115
+ }
116
+ if (_id !== undefined) {
117
+ return _id === testProjectId
118
+ }
119
+ return name === testProjectId
120
+ })?.name
121
+ }
122
+
123
+ function formatTestHookError (error, hookType, isTimeout) {
124
+ let hookError = error
125
+ if (error) {
126
+ hookError.message = `Error in ${hookType} hook: ${error.message}`
127
+ }
128
+ if (!hookError && isTimeout) {
129
+ hookError = new Error(`${hookType} hook timed out`)
130
+ }
131
+ return hookError
132
+ }
133
+
134
+ function addErrorToTestSuite (testSuiteAbsolutePath, error) {
135
+ if (testSuiteToErrors.has(testSuiteAbsolutePath)) {
136
+ testSuiteToErrors.get(testSuiteAbsolutePath).push(error)
137
+ } else {
138
+ testSuiteToErrors.set(testSuiteAbsolutePath, [error])
139
+ }
140
+ }
141
+
142
+ function getTestSuiteError (testSuiteAbsolutePath) {
143
+ const errors = testSuiteToErrors.get(testSuiteAbsolutePath)
144
+ if (!errors) {
145
+ return null
146
+ }
147
+ if (errors.length === 1) {
148
+ return errors[0]
149
+ }
150
+ return new Error(`${errors.length} errors in this test suite:\n${errors.map(e => e.message).join('\n------\n')}`)
103
151
  }
104
152
 
105
153
  function testBeginHandler (test, browserName) {
@@ -131,7 +179,7 @@ function testBeginHandler (test, browserName) {
131
179
  })
132
180
  }
133
181
 
134
- function testEndHandler (test, annotations, testStatus, error) {
182
+ function testEndHandler (test, annotations, testStatus, error, isTimeout) {
135
183
  let annotationTags
136
184
  if (annotations.length) {
137
185
  annotationTags = parseAnnotations(annotations)
@@ -139,6 +187,11 @@ function testEndHandler (test, annotations, testStatus, error) {
139
187
  const { _requireFile: testSuiteAbsolutePath, results, _type } = test
140
188
 
141
189
  if (_type === 'beforeAll' || _type === 'afterAll') {
190
+ const hookError = formatTestHookError(error, _type, isTimeout)
191
+
192
+ if (hookError) {
193
+ addErrorToTestSuite(testSuiteAbsolutePath, hookError)
194
+ }
142
195
  return
143
196
  }
144
197
 
@@ -148,15 +201,20 @@ function testEndHandler (test, annotations, testStatus, error) {
148
201
  testFinishCh.publish({ testStatus, steps: testResult.steps, error, extraTags: annotationTags })
149
202
  })
150
203
 
151
- if (!testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
152
- testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus])
153
- } else {
204
+ if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
154
205
  testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus)
206
+ } else {
207
+ testSuiteToTestStatuses.set(testSuiteAbsolutePath, [testStatus])
208
+ }
209
+
210
+ if (error) {
211
+ addErrorToTestSuite(testSuiteAbsolutePath, error)
155
212
  }
156
213
 
157
214
  remainingTestsByFile[testSuiteAbsolutePath] = remainingTestsByFile[testSuiteAbsolutePath]
158
215
  .filter(currentTest => currentTest !== test)
159
216
 
217
+ // Last test, we finish the suite
160
218
  if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
161
219
  const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
162
220
 
@@ -167,9 +225,10 @@ function testEndHandler (test, annotations, testStatus, error) {
167
225
  testSuiteStatus = 'skip'
168
226
  }
169
227
 
228
+ const suiteError = getTestSuiteError(testSuiteAbsolutePath)
170
229
  const testSuiteAsyncResource = testSuiteToAr.get(testSuiteAbsolutePath)
171
230
  testSuiteAsyncResource.runInAsyncScope(() => {
172
- testSuiteFinishCh.publish(testSuiteStatus)
231
+ testSuiteFinishCh.publish({ status: testSuiteStatus, error: suiteError })
173
232
  })
174
233
  }
175
234
  }
@@ -197,7 +256,7 @@ function dispatcherHook (dispatcherExport) {
197
256
  if (method === 'testBegin') {
198
257
  const { test } = dispatcher._testById.get(params.testId)
199
258
  const projects = getProjectsFromDispatcher(dispatcher)
200
- const browser = getBrowserNameFromProjects(projects, test._projectId)
259
+ const browser = getBrowserNameFromProjects(projects, test)
201
260
  testBeginHandler(test, browser)
202
261
  } else if (method === 'testEnd') {
203
262
  const { test } = dispatcher._testById.get(params.testId)
@@ -205,7 +264,8 @@ function dispatcherHook (dispatcherExport) {
205
264
  const { results } = test
206
265
  const testResult = results[results.length - 1]
207
266
 
208
- testEndHandler(test, params.annotations, STATUS_TO_TEST_STATUS[testResult.status], testResult.error)
267
+ const isTimeout = testResult.status === 'timedOut'
268
+ testEndHandler(test, params.annotations, STATUS_TO_TEST_STATUS[testResult.status], testResult.error, isTimeout)
209
269
  }
210
270
  })
211
271
 
@@ -232,13 +292,14 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
232
292
  worker.on('testBegin', ({ testId }) => {
233
293
  const test = getTestByTestId(dispatcher, testId)
234
294
  const projects = getProjectsFromDispatcher(dispatcher)
235
- const browser = getBrowserNameFromProjects(projects, test._projectId)
295
+ const browser = getBrowserNameFromProjects(projects, test)
236
296
  testBeginHandler(test, browser)
237
297
  })
238
298
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
239
299
  const test = getTestByTestId(dispatcher, testId)
240
300
 
241
- testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0])
301
+ const isTimeout = status === 'timedOut'
302
+ testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout)
242
303
  })
243
304
 
244
305
  return worker
@@ -265,7 +326,7 @@ function runnerHook (runnerExport, playwrightVersion) {
265
326
  // there were tests that did not go through `testBegin` or `testEnd`,
266
327
  // because they were skipped
267
328
  tests.forEach(test => {
268
- const browser = getBrowserNameFromProjects(projects, test._projectId)
329
+ const browser = getBrowserNameFromProjects(projects, test)
269
330
  testBeginHandler(test, browser)
270
331
  testEndHandler(test, [], 'skip')
271
332
  })
@@ -327,6 +388,7 @@ addHook({
327
388
  file: 'lib/runner/runner.js',
328
389
  versions: ['>=1.38.0']
329
390
  }, runnerHook)
391
+
330
392
  addHook({
331
393
  name: 'playwright',
332
394
  file: 'lib/runner/dispatcher.js',
@@ -2,7 +2,8 @@
2
2
 
3
3
  const { TEXT_MAP } = require('../../../ext/formats')
4
4
  const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
5
- const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor')
5
+ const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor')
6
+ const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway')
6
7
  const { getResourceName } = require('./util')
7
8
 
8
9
  class AmqplibConsumerPlugin extends ConsumerPlugin {
@@ -28,10 +29,13 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
28
29
  }
29
30
  })
30
31
 
31
- if (this.config.dsmEnabled && message) {
32
+ if (
33
+ this.config.dsmEnabled && message?.properties?.headers &&
34
+ DsmPathwayCodec.contextExists(message.properties.headers)
35
+ ) {
32
36
  const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content })
33
- const queue = fields.queue ?? fields.routingKey
34
- this.tracer.decodeDataStreamsContext(message.properties.headers[CONTEXT_PROPAGATION_KEY])
37
+ const queue = fields.queue ? fields.queue : fields.routingKey
38
+ this.tracer.decodeDataStreamsContext(message.properties.headers)
35
39
  this.tracer
36
40
  .setCheckpoint(['direction:in', `topic:${queue}`, 'type:rabbitmq'], span, payloadSize)
37
41
  }
@@ -3,8 +3,8 @@
3
3
  const { TEXT_MAP } = require('../../../ext/formats')
4
4
  const { CLIENT_PORT_KEY } = require('../../dd-trace/src/constants')
5
5
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
6
- const { encodePathwayContext } = require('../../dd-trace/src/datastreams/pathway')
7
- const { getAmqpMessageSize, CONTEXT_PROPAGATION_KEY } = require('../../dd-trace/src/datastreams/processor')
6
+ const { DsmPathwayCodec } = require('../../dd-trace/src/datastreams/pathway')
7
+ const { getAmqpMessageSize } = require('../../dd-trace/src/datastreams/processor')
8
8
  const { getResourceName } = require('./util')
9
9
 
10
10
  class AmqplibProducerPlugin extends ProducerPlugin {
@@ -40,8 +40,7 @@ class AmqplibProducerPlugin extends ProducerPlugin {
40
40
  .setCheckpoint(
41
41
  ['direction:out', `exchange:${fields.exchange}`, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq']
42
42
  , span, payloadSize)
43
- const pathwayCtx = encodePathwayContext(dataStreamsContext)
44
- fields.headers[CONTEXT_PROPAGATION_KEY] = pathwayCtx
43
+ DsmPathwayCodec.encode(dataStreamsContext, fields.headers)
45
44
  }
46
45
  }
47
46
  }
@@ -126,8 +126,9 @@ class BaseAwsSdkPlugin extends ClientPlugin {
126
126
  if (err) {
127
127
  span.setTag('error', err)
128
128
 
129
- if (err.requestId) {
130
- span.addTags({ 'aws.response.request_id': err.requestId })
129
+ const requestId = err.RequestId || err.requestId
130
+ if (requestId) {
131
+ span.addTags({ 'aws.response.request_id': requestId })
131
132
  }
132
133
  }
133
134
 
@@ -1,9 +1,8 @@
1
1
  'use strict'
2
2
  const {
3
- CONTEXT_PROPAGATION_KEY,
4
3
  getSizeOrZero
5
4
  } = require('../../../dd-trace/src/datastreams/processor')
6
- const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
5
+ const { DsmPathwayCodec } = require('../../../dd-trace/src/datastreams/pathway')
7
6
  const log = require('../../../dd-trace/src/log')
8
7
  const BaseAwsSdkPlugin = require('../base')
9
8
  const { storage } = require('../../../datadog-core')
@@ -53,7 +52,7 @@ class Kinesis extends BaseAwsSdkPlugin {
53
52
 
54
53
  // extract DSM context after as we might not have a parent-child but may have a DSM context
55
54
  this.responseExtractDSMContext(
56
- request.operation, response, span ?? null, streamName
55
+ request.operation, response, span || null, streamName
57
56
  )
58
57
  }
59
58
  })
@@ -113,13 +112,10 @@ class Kinesis extends BaseAwsSdkPlugin {
113
112
  const parsedAttributes = JSON.parse(Buffer.from(record.Data).toString())
114
113
 
115
114
  if (
116
- parsedAttributes &&
117
- parsedAttributes._datadog &&
118
- parsedAttributes._datadog[CONTEXT_PROPAGATION_KEY] &&
119
- streamName
115
+ parsedAttributes?._datadog && streamName && DsmPathwayCodec.contextExists(parsedAttributes._datadog)
120
116
  ) {
121
117
  const payloadSize = getSizeOrZero(record.Data)
122
- this.tracer.decodeDataStreamsContext(Buffer.from(parsedAttributes._datadog[CONTEXT_PROPAGATION_KEY]))
118
+ this.tracer.decodeDataStreamsContext(parsedAttributes._datadog)
123
119
  this.tracer
124
120
  .setCheckpoint(['direction:in', `topic:${streamName}`, 'type:kinesis'], span, payloadSize)
125
121
  }
@@ -143,62 +139,69 @@ class Kinesis extends BaseAwsSdkPlugin {
143
139
  }
144
140
 
145
141
  requestInject (span, request) {
146
- const operation = request.operation
147
- if (operation === 'putRecord' || operation === 'putRecords') {
148
- if (!request.params) {
149
- return
150
- }
151
- const traceData = {}
152
-
153
- // inject data with DD context
154
- this.tracer.inject(span, 'text_map', traceData)
155
- let injectPath
156
- if (request.params.Records && request.params.Records.length > 0) {
157
- injectPath = request.params.Records[0]
158
- } else if (request.params.Data) {
159
- injectPath = request.params
160
- } else {
161
- log.error('No valid payload passed, unable to pass trace context')
142
+ const { operation, params } = request
143
+ if (!params) return
144
+
145
+ let stream
146
+ switch (operation) {
147
+ case 'putRecord':
148
+ stream = params.StreamArn ? params.StreamArn : (params.StreamName ? params.StreamName : '')
149
+ this.injectToMessage(span, params, stream, true)
150
+ break
151
+ case 'putRecords':
152
+ stream = params.StreamArn ? params.StreamArn : (params.StreamName ? params.StreamName : '')
153
+ for (let i = 0; i < params.Records.length; i++) {
154
+ this.injectToMessage(span, params.Records[i], stream, i === 0)
155
+ }
156
+ }
157
+ }
158
+
159
+ injectToMessage (span, params, stream, injectTraceContext) {
160
+ if (!params) {
161
+ return
162
+ }
163
+
164
+ let parsedData
165
+ if (injectTraceContext || this.config.dsmEnabled) {
166
+ parsedData = this._tryParse(params.Data)
167
+ if (!parsedData) {
168
+ log.error('Unable to parse payload, unable to pass trace context or set DSM checkpoint (if enabled)')
162
169
  return
163
170
  }
171
+ }
164
172
 
165
- const parsedData = this._tryParse(injectPath.Data)
166
- if (parsedData) {
167
- parsedData._datadog = traceData
168
-
169
- // set DSM hash if enabled
170
- if (this.config.dsmEnabled) {
171
- // get payload size of request data
172
- const payloadSize = Buffer.from(JSON.stringify(parsedData)).byteLength
173
- let stream
174
- // users can optionally use either stream name or stream arn
175
- if (request.params && request.params.StreamArn) {
176
- stream = request.params.StreamArn
177
- } else if (request.params && request.params.StreamName) {
178
- stream = request.params.StreamName
179
- }
180
- const dataStreamsContext = this.tracer
181
- .setCheckpoint(['direction:out', `topic:${stream}`, 'type:kinesis'], span, payloadSize)
182
- if (dataStreamsContext) {
183
- const pathwayCtx = encodePathwayContext(dataStreamsContext)
184
- parsedData._datadog[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON()
185
- }
186
- }
173
+ const ddInfo = {}
174
+ // for now, we only want to inject to the first message, this may change for batches in the future
175
+ if (injectTraceContext) { this.tracer.inject(span, 'text_map', ddInfo) }
187
176
 
188
- const finalData = Buffer.from(JSON.stringify(parsedData))
189
- const byteSize = finalData.length
190
- // Kinesis max payload size is 1MB
191
- // So we must ensure adding DD context won't go over that (512b is an estimate)
192
- if (byteSize >= 1048576) {
193
- log.info('Payload size too large to pass context')
194
- return
195
- }
196
- injectPath.Data = finalData
197
- } else {
198
- log.error('Unable to parse payload, unable to pass trace context')
177
+ // set DSM hash if enabled
178
+ if (this.config.dsmEnabled) {
179
+ parsedData._datadog = ddInfo
180
+ const dataStreamsContext = this.setDSMCheckpoint(span, parsedData, stream)
181
+ DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
182
+ }
183
+
184
+ if (Object.keys(ddInfo).length !== 0) {
185
+ parsedData._datadog = ddInfo
186
+ const finalData = Buffer.from(JSON.stringify(parsedData))
187
+ const byteSize = finalData.length
188
+ // Kinesis max payload size is 1MB
189
+ // So we must ensure adding DD context won't go over that (512b is an estimate)
190
+ if (byteSize >= 1048576) {
191
+ log.info('Payload size too large to pass context')
192
+ return
199
193
  }
194
+ params.Data = finalData
200
195
  }
201
196
  }
197
+
198
+ setDSMCheckpoint (span, parsedData, stream) {
199
+ // get payload size of request data
200
+ const payloadSize = Buffer.from(JSON.stringify(parsedData)).byteLength
201
+ const dataStreamsContext = this.tracer
202
+ .setCheckpoint(['direction:out', `topic:${stream}`, 'type:kinesis'], span, payloadSize)
203
+ return dataStreamsContext
204
+ }
202
205
  }
203
206
 
204
207
  module.exports = Kinesis
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
- const { CONTEXT_PROPAGATION_KEY, getHeadersSize } = require('../../../dd-trace/src/datastreams/processor')
3
- const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
2
+ const { getHeadersSize } = require('../../../dd-trace/src/datastreams/processor')
3
+ const { DsmPathwayCodec } = require('../../../dd-trace/src/datastreams/pathway')
4
4
  const log = require('../../../dd-trace/src/log')
5
5
  const BaseAwsSdkPlugin = require('../base')
6
6
 
@@ -55,17 +55,17 @@ class Sns extends BaseAwsSdkPlugin {
55
55
 
56
56
  switch (operation) {
57
57
  case 'publish':
58
- this._injectMessageAttributes(span, params)
58
+ this.injectToMessage(span, params, params.TopicArn, true)
59
59
  break
60
60
  case 'publishBatch':
61
- if (params.PublishBatchRequestEntries && params.PublishBatchRequestEntries.length > 0) {
62
- this._injectMessageAttributes(span, params.PublishBatchRequestEntries[0])
61
+ for (let i = 0; i < params.PublishBatchRequestEntries.length; i++) {
62
+ this.injectToMessage(span, params.PublishBatchRequestEntries[i], params.TopicArn, i === 0)
63
63
  }
64
64
  break
65
65
  }
66
66
  }
67
67
 
68
- _injectMessageAttributes (span, params) {
68
+ injectToMessage (span, params, topicArn, injectTraceContext) {
69
69
  if (!params.MessageAttributes) {
70
70
  params.MessageAttributes = {}
71
71
  }
@@ -73,27 +73,47 @@ class Sns extends BaseAwsSdkPlugin {
73
73
  log.info('Message attributes full, skipping trace context injection')
74
74
  return
75
75
  }
76
+
76
77
  const ddInfo = {}
77
- this.tracer.inject(span, 'text_map', ddInfo)
78
- // add ddInfo before checking DSM so we can include DD attributes in payload size
79
- params.MessageAttributes._datadog = {
80
- DataType: 'Binary',
81
- BinaryValue: ddInfo
78
+ // for now, we only want to inject to the first message, this may change for batches in the future
79
+ if (injectTraceContext) {
80
+ this.tracer.inject(span, 'text_map', ddInfo)
81
+ // add ddInfo before checking DSM so we can include DD attributes in payload size
82
+ params.MessageAttributes._datadog = {
83
+ DataType: 'Binary',
84
+ BinaryValue: ddInfo
85
+ }
82
86
  }
87
+
83
88
  if (this.config.dsmEnabled) {
84
- const payloadSize = getHeadersSize({
85
- Message: params.Message,
86
- MessageAttributes: params.MessageAttributes
87
- })
88
- const dataStreamsContext = this.tracer
89
- .setCheckpoint(['direction:out', `topic:${params.TopicArn}`, 'type:sns'], span, payloadSize)
90
- if (dataStreamsContext) {
91
- const pathwayCtx = encodePathwayContext(dataStreamsContext)
92
- ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON()
89
+ if (!params.MessageAttributes._datadog) {
90
+ params.MessageAttributes._datadog = {
91
+ DataType: 'Binary',
92
+ BinaryValue: ddInfo
93
+ }
93
94
  }
95
+
96
+ const dataStreamsContext = this.setDSMCheckpoint(span, params, topicArn)
97
+ DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
98
+ }
99
+
100
+ if (Object.keys(ddInfo).length !== 0) {
101
+ // BINARY types are automatically base64 encoded
102
+ params.MessageAttributes._datadog.BinaryValue = Buffer.from(JSON.stringify(ddInfo))
103
+ } else if (params.MessageAttributes._datadog) {
104
+ // let's avoid adding any additional information to payload if we failed to inject
105
+ delete params.MessageAttributes._datadog
106
+ }
107
+ }
108
+
109
+ setDSMCheckpoint (span, params, topicArn) {
110
+ // only set a checkpoint if publishing to a topic
111
+ if (topicArn) {
112
+ const payloadSize = getHeadersSize(params)
113
+ const dataStreamsContext = this.tracer
114
+ .setCheckpoint(['direction:out', `topic:${topicArn}`, 'type:sns'], span, payloadSize)
115
+ return dataStreamsContext
94
116
  }
95
- // BINARY types are automatically base64 encoded
96
- params.MessageAttributes._datadog.BinaryValue = Buffer.from(JSON.stringify(ddInfo))
97
117
  }
98
118
  }
99
119
 
@@ -3,8 +3,8 @@
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
5
  const { storage } = require('../../../datadog-core')
6
- const { CONTEXT_PROPAGATION_KEY, getHeadersSize } = require('../../../dd-trace/src/datastreams/processor')
7
- const { encodePathwayContext } = require('../../../dd-trace/src/datastreams/pathway')
6
+ const { getHeadersSize } = require('../../../dd-trace/src/datastreams/processor')
7
+ const { DsmPathwayCodec } = require('../../../dd-trace/src/datastreams/pathway')
8
8
 
9
9
  class Sqs extends BaseAwsSdkPlugin {
10
10
  static get id () { return 'sqs' }
@@ -40,7 +40,7 @@ class Sqs extends BaseAwsSdkPlugin {
40
40
  }
41
41
  // extract DSM context after as we might not have a parent-child but may have a DSM context
42
42
  this.responseExtractDSMContext(
43
- request.operation, request.params, response, span ?? null, parsedMessageAttributes ?? null
43
+ request.operation, request.params, response, span || null, parsedMessageAttributes || null
44
44
  )
45
45
  })
46
46
 
@@ -192,13 +192,13 @@ class Sqs extends BaseAwsSdkPlugin {
192
192
  parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
193
193
  }
194
194
  }
195
- if (parsedAttributes && parsedAttributes[CONTEXT_PROPAGATION_KEY]) {
195
+ if (parsedAttributes && DsmPathwayCodec.contextExists(parsedAttributes)) {
196
196
  const payloadSize = getHeadersSize({
197
197
  Body: message.Body,
198
198
  MessageAttributes: message.MessageAttributes
199
199
  })
200
200
  const queue = params.QueueUrl.split('/').pop()
201
- this.tracer.decodeDataStreamsContext(Buffer.from(parsedAttributes[CONTEXT_PROPAGATION_KEY]))
201
+ this.tracer.decodeDataStreamsContext(parsedAttributes)
202
202
  this.tracer
203
203
  .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
204
204
  }
@@ -206,39 +206,73 @@ class Sqs extends BaseAwsSdkPlugin {
206
206
  }
207
207
 
208
208
  requestInject (span, request) {
209
- const operation = request.operation
210
- if (operation === 'sendMessage') {
211
- if (!request.params) {
212
- request.params = {}
213
- }
214
- if (!request.params.MessageAttributes) {
215
- request.params.MessageAttributes = {}
216
- } else if (Object.keys(request.params.MessageAttributes).length >= 10) { // SQS quota
217
- // TODO: add test when the test suite is fixed
218
- return
219
- }
220
- const ddInfo = {}
209
+ const { operation, params } = request
210
+
211
+ if (!params) return
212
+
213
+ switch (operation) {
214
+ case 'sendMessage':
215
+ this.injectToMessage(span, params, params.QueueUrl, true)
216
+ break
217
+ case 'sendMessageBatch':
218
+ for (let i = 0; i < params.Entries.length; i++) {
219
+ this.injectToMessage(span, params.Entries[i], params.QueueUrl, i === 0)
220
+ }
221
+ break
222
+ }
223
+ }
224
+
225
+ injectToMessage (span, params, queueUrl, injectTraceContext) {
226
+ if (!params) {
227
+ params = {}
228
+ }
229
+ if (!params.MessageAttributes) {
230
+ params.MessageAttributes = {}
231
+ } else if (Object.keys(params.MessageAttributes).length >= 10) { // SQS quota
232
+ // TODO: add test when the test suite is fixed
233
+ return
234
+ }
235
+ const ddInfo = {}
236
+ // for now, we only want to inject to the first message, this may change for batches in the future
237
+ if (injectTraceContext) {
221
238
  this.tracer.inject(span, 'text_map', ddInfo)
222
- request.params.MessageAttributes._datadog = {
239
+ params.MessageAttributes._datadog = {
223
240
  DataType: 'String',
224
241
  StringValue: JSON.stringify(ddInfo)
225
242
  }
226
- if (this.config.dsmEnabled) {
227
- const payloadSize = getHeadersSize({
228
- Body: request.params.MessageBody,
229
- MessageAttributes: request.params.MessageAttributes
230
- })
231
- const queue = request.params.QueueUrl.split('/').pop()
232
- const dataStreamsContext = this.tracer
233
- .setCheckpoint(['direction:out', `topic:${queue}`, 'type:sqs'], span, payloadSize)
234
- if (dataStreamsContext) {
235
- const pathwayCtx = encodePathwayContext(dataStreamsContext)
236
- ddInfo[CONTEXT_PROPAGATION_KEY] = pathwayCtx.toJSON()
243
+ }
244
+
245
+ if (this.config.dsmEnabled) {
246
+ if (!params.MessageAttributes._datadog) {
247
+ params.MessageAttributes._datadog = {
248
+ DataType: 'String',
249
+ StringValue: JSON.stringify(ddInfo)
237
250
  }
238
251
  }
239
- request.params.MessageAttributes._datadog.StringValue = JSON.stringify(ddInfo)
252
+
253
+ const dataStreamsContext = this.setDSMCheckpoint(span, params, queueUrl)
254
+ if (dataStreamsContext) {
255
+ DsmPathwayCodec.encode(dataStreamsContext, ddInfo)
256
+ params.MessageAttributes._datadog.StringValue = JSON.stringify(ddInfo)
257
+ }
258
+ }
259
+
260
+ if (params.MessageAttributes._datadog && Object.keys(ddInfo).length === 0) {
261
+ // let's avoid adding any additional information to payload if we failed to inject
262
+ delete params.MessageAttributes._datadog
240
263
  }
241
264
  }
265
+
266
+ setDSMCheckpoint (span, params, queueUrl) {
267
+ const payloadSize = getHeadersSize({
268
+ Body: params.MessageBody,
269
+ MessageAttributes: params.MessageAttributes
270
+ })
271
+ const queue = queueUrl.split('/').pop()
272
+ const dataStreamsContext = this.tracer
273
+ .setCheckpoint(['direction:out', `topic:${queue}`, 'type:sqs'], span, payloadSize)
274
+ return dataStreamsContext
275
+ }
242
276
  }
243
277
 
244
278
  module.exports = Sqs