dd-trace 5.16.0 → 5.17.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.
package/index.d.ts CHANGED
@@ -197,6 +197,7 @@ interface Plugins {
197
197
  "selenium": tracer.plugins.selenium;
198
198
  "sharedb": tracer.plugins.sharedb;
199
199
  "tedious": tracer.plugins.tedious;
200
+ "undici": tracer.plugins.undici;
200
201
  "winston": tracer.plugins.winston;
201
202
  }
202
203
 
@@ -1800,6 +1801,12 @@ declare namespace tracer {
1800
1801
  */
1801
1802
  interface tedious extends Instrumentation {}
1802
1803
 
1804
+ /**
1805
+ * This plugin automatically instruments the
1806
+ * [undici](https://github.com/nodejs/undici) module.
1807
+ */
1808
+ interface undici extends HttpClient {}
1809
+
1803
1810
  /**
1804
1811
  * This plugin patches the [winston](https://github.com/winstonjs/winston)
1805
1812
  * to automatically inject trace identifiers in log records when the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.16.0",
3
+ "version": "5.17.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -109,6 +109,7 @@ module.exports = {
109
109
  sequelize: () => require('../sequelize'),
110
110
  sharedb: () => require('../sharedb'),
111
111
  tedious: () => require('../tedious'),
112
+ undici: () => require('../undici'),
112
113
  when: () => require('../when'),
113
114
  winston: () => require('../winston')
114
115
  }
@@ -10,6 +10,7 @@ const startServerCh = channel('apm:http:server:request:start')
10
10
  const exitServerCh = channel('apm:http:server:request:exit')
11
11
  const errorServerCh = channel('apm:http:server:request:error')
12
12
  const finishServerCh = channel('apm:http:server:request:finish')
13
+ const startWriteHeadCh = channel('apm:http:server:response:writeHead:start')
13
14
  const finishSetHeaderCh = channel('datadog:http:server:response:set-header:finish')
14
15
 
15
16
  const requestFinishedSet = new WeakSet()
@@ -20,6 +21,9 @@ const httpsNames = ['https', 'node:https']
20
21
  addHook({ name: httpNames }, http => {
21
22
  shimmer.wrap(http.ServerResponse.prototype, 'emit', wrapResponseEmit)
22
23
  shimmer.wrap(http.Server.prototype, 'emit', wrapEmit)
24
+ shimmer.wrap(http.ServerResponse.prototype, 'writeHead', wrapWriteHead)
25
+ shimmer.wrap(http.ServerResponse.prototype, 'write', wrapWrite)
26
+ shimmer.wrap(http.ServerResponse.prototype, 'end', wrapEnd)
23
27
  return http
24
28
  })
25
29
 
@@ -86,3 +90,97 @@ function wrapSetHeader (res) {
86
90
  }
87
91
  })
88
92
  }
93
+
94
+ function wrapWriteHead (writeHead) {
95
+ return function wrappedWriteHead (statusCode, reason, obj) {
96
+ if (!startWriteHeadCh.hasSubscribers) {
97
+ return writeHead.apply(this, arguments)
98
+ }
99
+
100
+ const abortController = new AbortController()
101
+
102
+ if (typeof reason !== 'string') {
103
+ obj ??= reason
104
+ }
105
+
106
+ // support writeHead(200, ['key1', 'val1', 'key2', 'val2'])
107
+ if (Array.isArray(obj)) {
108
+ const headers = {}
109
+
110
+ for (let i = 0; i < obj.length; i += 2) {
111
+ headers[obj[i]] = obj[i + 1]
112
+ }
113
+
114
+ obj = headers
115
+ }
116
+
117
+ // this doesn't support explicit duplicate headers, but it's an edge case
118
+ const responseHeaders = Object.assign(this.getHeaders(), obj)
119
+
120
+ startWriteHeadCh.publish({
121
+ req: this.req,
122
+ res: this,
123
+ abortController,
124
+ statusCode,
125
+ responseHeaders
126
+ })
127
+
128
+ if (abortController.signal.aborted) {
129
+ return this
130
+ }
131
+
132
+ return writeHead.apply(this, arguments)
133
+ }
134
+ }
135
+
136
+ function wrapWrite (write) {
137
+ return function wrappedWrite () {
138
+ if (!startWriteHeadCh.hasSubscribers) {
139
+ return write.apply(this, arguments)
140
+ }
141
+
142
+ const abortController = new AbortController()
143
+
144
+ const responseHeaders = this.getHeaders()
145
+
146
+ startWriteHeadCh.publish({
147
+ req: this.req,
148
+ res: this,
149
+ abortController,
150
+ statusCode: this.statusCode,
151
+ responseHeaders
152
+ })
153
+
154
+ if (abortController.signal.aborted) {
155
+ return true
156
+ }
157
+
158
+ return write.apply(this, arguments)
159
+ }
160
+ }
161
+
162
+ function wrapEnd (end) {
163
+ return function wrappedEnd () {
164
+ if (!startWriteHeadCh.hasSubscribers) {
165
+ return end.apply(this, arguments)
166
+ }
167
+
168
+ const abortController = new AbortController()
169
+
170
+ const responseHeaders = this.getHeaders()
171
+
172
+ startWriteHeadCh.publish({
173
+ req: this.req,
174
+ res: this,
175
+ abortController,
176
+ statusCode: this.statusCode,
177
+ responseHeaders
178
+ })
179
+
180
+ if (abortController.signal.aborted) {
181
+ return this
182
+ }
183
+
184
+ return end.apply(this, arguments)
185
+ }
186
+ }
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ addHook
5
+ } = require('./helpers/instrument')
6
+ const shimmer = require('../../datadog-shimmer')
7
+
8
+ const tracingChannel = require('dc-polyfill').tracingChannel
9
+ const ch = tracingChannel('apm:undici:fetch')
10
+
11
+ const { createWrapFetch } = require('./helpers/fetch')
12
+
13
+ addHook({
14
+ name: 'undici',
15
+ versions: ['^4.4.1', '5', '>=6.0.0']
16
+ }, undici => {
17
+ return shimmer.wrap(undici, 'fetch', createWrapFetch(undici.Request, ch))
18
+ })
@@ -0,0 +1,12 @@
1
+ 'use strict'
2
+
3
+ const FetchPlugin = require('../../datadog-plugin-fetch/src/index.js')
4
+
5
+ class UndiciPlugin extends FetchPlugin {
6
+ static get id () { return 'undici' }
7
+ static get prefix () {
8
+ return 'tracing:apm:undici:fetch'
9
+ }
10
+ }
11
+
12
+ module.exports = UndiciPlugin
@@ -111,6 +111,10 @@ function block (req, res, rootSpan, abortController, actionParameters) {
111
111
 
112
112
  const { body, headers, statusCode } = getBlockingData(req, null, rootSpan, actionParameters)
113
113
 
114
+ for (const headerName of res.getHeaderNames()) {
115
+ res.removeHeader(headerName)
116
+ }
117
+
114
118
  res.writeHead(statusCode, headers).end(body)
115
119
 
116
120
  abortController?.abort()
@@ -18,5 +18,6 @@ module.exports = {
18
18
  nextBodyParsed: dc.channel('apm:next:body-parsed'),
19
19
  nextQueryParsed: dc.channel('apm:next:query-parsed'),
20
20
  responseBody: dc.channel('datadog:express:response:json:start'),
21
+ responseWriteHead: dc.channel('apm:http:server:response:writeHead:start'),
21
22
  httpClientRequestStart: dc.channel('apm:http:client:request:start')
22
23
  }
@@ -12,7 +12,8 @@ const {
12
12
  queryParser,
13
13
  nextBodyParsed,
14
14
  nextQueryParsed,
15
- responseBody
15
+ responseBody,
16
+ responseWriteHead
16
17
  } = require('./channels')
17
18
  const waf = require('./waf')
18
19
  const addresses = require('./addresses')
@@ -60,6 +61,7 @@ function enable (_config) {
60
61
  queryParser.subscribe(onRequestQueryParsed)
61
62
  cookieParser.subscribe(onRequestCookieParser)
62
63
  responseBody.subscribe(onResponseBody)
64
+ responseWriteHead.subscribe(onResponseWriteHead)
63
65
 
64
66
  if (_config.appsec.eventTracking.enabled) {
65
67
  passportVerify.subscribe(onPassportVerify)
@@ -110,14 +112,7 @@ function incomingHttpStartTranslator ({ req, res, abortController }) {
110
112
  }
111
113
 
112
114
  function incomingHttpEndTranslator ({ req, res }) {
113
- // TODO: this doesn't support headers sent with res.writeHead()
114
- const responseHeaders = Object.assign({}, res.getHeaders())
115
- delete responseHeaders['set-cookie']
116
-
117
- const persistent = {
118
- [addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + res.statusCode,
119
- [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
120
- }
115
+ const persistent = {}
121
116
 
122
117
  // we need to keep this to support other body parsers
123
118
  // TODO: no need to analyze it if it was already done by the body-parser hook
@@ -139,7 +134,9 @@ function incomingHttpEndTranslator ({ req, res }) {
139
134
  persistent[addresses.HTTP_INCOMING_QUERY] = req.query
140
135
  }
141
136
 
142
- waf.run({ persistent }, req)
137
+ if (Object.keys(persistent).length) {
138
+ waf.run({ persistent }, req)
139
+ }
143
140
 
144
141
  waf.disposeContext(req)
145
142
 
@@ -225,12 +222,48 @@ function onPassportVerify ({ credentials, user }) {
225
222
  passportTrackEvent(credentials, user, rootSpan, config.appsec.eventTracking.mode)
226
223
  }
227
224
 
225
+ const responseAnalyzedSet = new WeakSet()
226
+ const responseBlockedSet = new WeakSet()
227
+
228
+ function onResponseWriteHead ({ req, res, abortController, statusCode, responseHeaders }) {
229
+ // avoid "write after end" error
230
+ if (responseBlockedSet.has(res)) {
231
+ abortController?.abort()
232
+ return
233
+ }
234
+
235
+ // avoid double waf call
236
+ if (responseAnalyzedSet.has(res)) {
237
+ return
238
+ }
239
+
240
+ const rootSpan = web.root(req)
241
+ if (!rootSpan) return
242
+
243
+ responseHeaders = Object.assign({}, responseHeaders)
244
+ delete responseHeaders['set-cookie']
245
+
246
+ const results = waf.run({
247
+ persistent: {
248
+ [addresses.HTTP_INCOMING_RESPONSE_CODE]: '' + statusCode,
249
+ [addresses.HTTP_INCOMING_RESPONSE_HEADERS]: responseHeaders
250
+ }
251
+ }, req)
252
+
253
+ responseAnalyzedSet.add(res)
254
+
255
+ handleResults(results, req, res, rootSpan, abortController)
256
+ }
257
+
228
258
  function handleResults (actions, req, res, rootSpan, abortController) {
229
259
  if (!actions || !req || !res || !rootSpan || !abortController) return
230
260
 
231
261
  const blockingAction = getBlockingAction(actions)
232
262
  if (blockingAction) {
233
263
  block(req, res, rootSpan, abortController, blockingAction)
264
+ if (!abortController.signal || abortController.signal.aborted) {
265
+ responseBlockedSet.add(res)
266
+ }
234
267
  }
235
268
  }
236
269
 
@@ -256,6 +289,7 @@ function disable () {
256
289
  if (cookieParser.hasSubscribers) cookieParser.unsubscribe(onRequestCookieParser)
257
290
  if (responseBody.hasSubscribers) responseBody.unsubscribe(onResponseBody)
258
291
  if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
292
+ if (responseWriteHead.hasSubscribers) responseWriteHead.unsubscribe(onResponseWriteHead)
259
293
  }
260
294
 
261
295
  module.exports = {
@@ -6,6 +6,7 @@ module.exports = {
6
6
  ASM_DD_RULES: 1n << 3n,
7
7
  ASM_EXCLUSIONS: 1n << 4n,
8
8
  ASM_REQUEST_BLOCKING: 1n << 5n,
9
+ ASM_RESPONSE_BLOCKING: 1n << 6n,
9
10
  ASM_USER_BLOCKING: 1n << 7n,
10
11
  ASM_CUSTOM_RULES: 1n << 8n,
11
12
  ASM_CUSTOM_BLOCKING_RESPONSE: 1n << 9n,
@@ -71,6 +71,7 @@ function enableWafUpdate (appsecConfig) {
71
71
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, true)
72
72
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, true)
73
73
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, true)
74
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, true)
74
75
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, true)
75
76
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, true)
76
77
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, true)
@@ -92,6 +93,7 @@ function disableWafUpdate () {
92
93
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_DD_RULES, false)
93
94
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_EXCLUSIONS, false)
94
95
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_REQUEST_BLOCKING, false)
96
+ rc.updateCapabilities(RemoteConfigCapabilities.ASM_RESPONSE_BLOCKING, false)
95
97
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_RULES, false)
96
98
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_CUSTOM_BLOCKING_RESPONSE, false)
97
99
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_TRUSTED_IPS, false)
@@ -34,6 +34,7 @@ function format (span) {
34
34
  const formatted = formatSpan(span)
35
35
 
36
36
  extractSpanLinks(formatted, span)
37
+ extractSpanEvents(formatted, span)
37
38
  extractRootTags(formatted, span)
38
39
  extractChunkTags(formatted, span)
39
40
  extractTags(formatted, span)
@@ -88,6 +89,22 @@ function extractSpanLinks (trace, span) {
88
89
  if (links.length > 0) { trace.meta['_dd.span_links'] = JSON.stringify(links) }
89
90
  }
90
91
 
92
+ function extractSpanEvents (trace, span) {
93
+ const events = []
94
+ if (span._events) {
95
+ for (const event of span._events) {
96
+ const formattedEvent = {
97
+ name: event.name,
98
+ time_unix_nano: Math.round(event.startTime * 1e6),
99
+ attributes: event.attributes && Object.keys(event.attributes).length > 0 ? event.attributes : undefined
100
+ }
101
+
102
+ events.push(formattedEvent)
103
+ }
104
+ }
105
+ if (events.length > 0) { trace.meta.events = JSON.stringify(events) }
106
+ }
107
+
91
108
  function extractTags (trace, span) {
92
109
  const context = span.context()
93
110
  const origin = context._trace.origin
@@ -134,7 +151,10 @@ function extractTags (trace, span) {
134
151
  case ERROR_STACK:
135
152
  // HACK: remove when implemented in the backend
136
153
  if (context._name !== 'fs.operation') {
137
- trace.error = 1
154
+ // HACK: to ensure otel.recordException does not influence trace.error
155
+ if (tags.setTraceError) {
156
+ trace.error = 1
157
+ }
138
158
  } else {
139
159
  break
140
160
  }
@@ -142,7 +162,6 @@ function extractTags (trace, span) {
142
162
  addTag(trace.meta, trace.metrics, tag, tags[tag])
143
163
  }
144
164
  }
145
-
146
165
  setSingleSpanIngestionTags(trace, context._spanSampling)
147
166
 
148
167
  addTag(trace.meta, trace.metrics, 'language', 'javascript')
@@ -20,6 +20,20 @@ function hrTimeToMilliseconds (time) {
20
20
  return time[0] * 1e3 + time[1] / 1e6
21
21
  }
22
22
 
23
+ function isTimeInput (startTime) {
24
+ if (typeof startTime === 'number') {
25
+ return true
26
+ }
27
+ if (startTime instanceof Date) {
28
+ return true
29
+ }
30
+ if (Array.isArray(startTime) && startTime.length === 2 &&
31
+ typeof startTime[0] === 'number' && typeof startTime[1] === 'number') {
32
+ return true
33
+ }
34
+ return false
35
+ }
36
+
23
37
  const spanKindNames = {
24
38
  [api.SpanKind.INTERNAL]: kinds.INTERNAL,
25
39
  [api.SpanKind.SERVER]: kinds.SERVER,
@@ -196,11 +210,6 @@ class Span {
196
210
  return this
197
211
  }
198
212
 
199
- addEvent (name, attributesOrStartTime, startTime) {
200
- api.diag.warn('Events not supported')
201
- return this
202
- }
203
-
204
213
  addLink (context, attributes) {
205
214
  // extract dd context
206
215
  const ddSpanContext = context._ddContext
@@ -244,12 +253,29 @@ class Span {
244
253
  return this.ended === false
245
254
  }
246
255
 
247
- recordException (exception) {
256
+ addEvent (name, attributesOrStartTime, startTime) {
257
+ startTime = attributesOrStartTime && isTimeInput(attributesOrStartTime) ? attributesOrStartTime : startTime
258
+ const hrStartTime = timeInputToHrTime(startTime || (performance.now() + timeOrigin))
259
+ startTime = hrTimeToMilliseconds(hrStartTime)
260
+
261
+ this._ddSpan.addEvent(name, attributesOrStartTime, startTime)
262
+ return this
263
+ }
264
+
265
+ recordException (exception, timeInput) {
266
+ // HACK: identifier is added so that trace.error remains unchanged after a call to otel.recordException
248
267
  this._ddSpan.addTags({
249
268
  [ERROR_TYPE]: exception.name,
250
269
  [ERROR_MESSAGE]: exception.message,
251
- [ERROR_STACK]: exception.stack
270
+ [ERROR_STACK]: exception.stack,
271
+ doNotSetTraceError: true
252
272
  })
273
+ const attributes = {}
274
+ if (exception.message) attributes['exception.message'] = exception.message
275
+ if (exception.type) attributes['exception.type'] = exception.type
276
+ if (exception.escaped) attributes['exception.escaped'] = exception.escaped
277
+ if (exception.stack) attributes['exception.stacktrace'] = exception.stack
278
+ this.addEvent(exception.name, attributes, timeInput)
253
279
  }
254
280
 
255
281
  get duration () {
@@ -67,6 +67,8 @@ class DatadogSpan {
67
67
  this._store = storage.getStore()
68
68
  this._duration = undefined
69
69
 
70
+ this._events = []
71
+
70
72
  // For internal use only. You probably want `context()._name`.
71
73
  // This name property is not updated when the span name changes.
72
74
  // This is necessary for span count metrics.
@@ -163,6 +165,19 @@ class DatadogSpan {
163
165
  })
164
166
  }
165
167
 
168
+ addEvent (name, attributesOrStartTime, startTime) {
169
+ const event = { name }
170
+ if (attributesOrStartTime) {
171
+ if (typeof attributesOrStartTime === 'object') {
172
+ event.attributes = this._sanitizeEventAttributes(attributesOrStartTime)
173
+ } else {
174
+ startTime = attributesOrStartTime
175
+ }
176
+ }
177
+ event.startTime = startTime || this._getTime()
178
+ this._events.push(event)
179
+ }
180
+
166
181
  finish (finishTime) {
167
182
  if (this._duration !== undefined) {
168
183
  return
@@ -221,7 +236,30 @@ class DatadogSpan {
221
236
  const [key, value] = entry
222
237
  addArrayOrScalarAttributes(key, value)
223
238
  })
239
+ return sanitizedAttributes
240
+ }
241
+
242
+ _sanitizeEventAttributes (attributes = {}) {
243
+ const sanitizedAttributes = {}
224
244
 
245
+ for (const key in attributes) {
246
+ const value = attributes[key]
247
+ if (Array.isArray(value)) {
248
+ const newArray = []
249
+ for (const subkey in value) {
250
+ if (ALLOWED.includes(typeof value[subkey])) {
251
+ newArray.push(value[subkey])
252
+ } else {
253
+ log.warn('Dropping span event attribute. It is not of an allowed type')
254
+ }
255
+ }
256
+ sanitizedAttributes[key] = newArray
257
+ } else if (ALLOWED.includes(typeof value)) {
258
+ sanitizedAttributes[key] = value
259
+ } else {
260
+ log.warn('Dropping span event attribute. It is not of an allowed type')
261
+ }
262
+ }
225
263
  return sanitizedAttributes
226
264
  }
227
265
 
@@ -81,5 +81,6 @@ module.exports = {
81
81
  get 'selenium-webdriver' () { return require('../../../datadog-plugin-selenium/src') },
82
82
  get sharedb () { return require('../../../datadog-plugin-sharedb/src') },
83
83
  get tedious () { return require('../../../datadog-plugin-tedious/src') },
84
+ get undici () { return require('../../../datadog-plugin-undici/src') },
84
85
  get winston () { return require('../../../datadog-plugin-winston/src') }
85
86
  }
@@ -30,6 +30,10 @@ const web = {
30
30
  lambda: {
31
31
  opName: () => 'aws.request',
32
32
  serviceName: awsServiceV0
33
+ },
34
+ undici: {
35
+ opName: () => 'undici.request',
36
+ serviceName: httpPluginClientService
33
37
  }
34
38
  },
35
39
  server: {
@@ -29,6 +29,10 @@ const web = {
29
29
  lambda: {
30
30
  opName: () => 'aws.lambda.invoke',
31
31
  serviceName: identityService
32
+ },
33
+ undici: {
34
+ opName: () => 'undici.request',
35
+ serviceName: httpPluginClientService
32
36
  }
33
37
  },
34
38
  server: {
@@ -1,6 +1,10 @@
1
1
  'use strict'
2
2
 
3
+ const constants = require('./constants')
3
4
  const log = require('./log')
5
+ const ERROR_MESSAGE = constants.ERROR_MESSAGE
6
+ const ERROR_STACK = constants.ERROR_STACK
7
+ const ERROR_TYPE = constants.ERROR_TYPE
4
8
 
5
9
  const otelTagMap = {
6
10
  'deployment.environment': 'env',
@@ -14,7 +18,6 @@ function add (carrier, keyValuePairs, parseOtelTags = false) {
14
18
  if (Array.isArray(keyValuePairs)) {
15
19
  return keyValuePairs.forEach(tags => add(carrier, tags))
16
20
  }
17
-
18
21
  try {
19
22
  if (typeof keyValuePairs === 'string') {
20
23
  const segments = keyValuePairs.split(',')
@@ -32,6 +35,12 @@ function add (carrier, keyValuePairs, parseOtelTags = false) {
32
35
  carrier[key.trim()] = value.trim()
33
36
  }
34
37
  } else {
38
+ // HACK: to ensure otel.recordException does not influence trace.error
39
+ if (ERROR_MESSAGE in keyValuePairs || ERROR_STACK in keyValuePairs || ERROR_TYPE in keyValuePairs) {
40
+ if (!('doNotSetTraceError' in keyValuePairs)) {
41
+ carrier.setTraceError = true
42
+ }
43
+ }
35
44
  Object.assign(carrier, keyValuePairs)
36
45
  }
37
46
  } catch (e) {