dd-trace 2.11.0 → 3.0.0-pre.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 (45) hide show
  1. package/LICENSE-3rdparty.csv +0 -1
  2. package/ci/init.js +2 -26
  3. package/ci/jest/env.js +2 -3
  4. package/index.d.ts +3 -10
  5. package/package.json +4 -5
  6. package/packages/datadog-core/src/storage/async_hooks.js +4 -4
  7. package/packages/datadog-core/src/storage/async_resource.js +14 -4
  8. package/packages/datadog-instrumentations/src/connect.js +4 -4
  9. package/packages/datadog-instrumentations/src/couchbase.js +166 -61
  10. package/packages/datadog-instrumentations/src/fastify.js +12 -25
  11. package/packages/datadog-instrumentations/src/graphql.js +17 -5
  12. package/packages/datadog-instrumentations/src/koa.js +4 -4
  13. package/packages/datadog-instrumentations/src/mocha.js +88 -18
  14. package/packages/datadog-instrumentations/src/restify.js +4 -8
  15. package/packages/datadog-instrumentations/src/router.js +4 -4
  16. package/packages/datadog-plugin-couchbase/src/index.js +8 -10
  17. package/packages/datadog-plugin-graphql/src/resolve.js +2 -0
  18. package/packages/datadog-plugin-grpc/src/client.js +2 -2
  19. package/packages/datadog-plugin-grpc/src/server.js +2 -2
  20. package/packages/datadog-plugin-http/src/client.js +1 -1
  21. package/packages/datadog-plugin-http/src/server.js +4 -9
  22. package/packages/datadog-plugin-http2/src/client.js +1 -1
  23. package/packages/datadog-plugin-mocha/src/index.js +80 -3
  24. package/packages/datadog-plugin-next/src/index.js +1 -1
  25. package/packages/datadog-plugin-router/src/index.js +39 -10
  26. package/packages/dd-trace/src/config.js +8 -0
  27. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +111 -15
  28. package/packages/dd-trace/src/exporters/common/request.js +43 -34
  29. package/packages/dd-trace/src/noop/tracer.js +0 -4
  30. package/packages/dd-trace/src/plugin_manager.js +53 -33
  31. package/packages/dd-trace/src/plugins/index.js +0 -1
  32. package/packages/dd-trace/src/plugins/util/test.js +32 -1
  33. package/packages/dd-trace/src/plugins/util/web.js +73 -20
  34. package/packages/dd-trace/src/profiling/config.js +10 -2
  35. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -2
  36. package/packages/dd-trace/src/profiling/exporters/form-data.js +53 -0
  37. package/packages/dd-trace/src/profiling/index.js +2 -0
  38. package/packages/dd-trace/src/profiling/profiler.js +6 -1
  39. package/packages/dd-trace/src/profiling/profilers/cpu.js +126 -0
  40. package/packages/dd-trace/src/proxy.js +0 -19
  41. package/packages/dd-trace/src/scope.js +1 -126
  42. package/packages/dd-trace/src/tracer.js +0 -4
  43. package/cypress/plugin.js +0 -5
  44. package/cypress/support.js +0 -1
  45. package/packages/datadog-plugin-fs/src/index.js +0 -548
@@ -3,13 +3,25 @@ const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
3
  const Chunk = require('./chunk')
4
4
  const { AgentEncoder } = require('./0.4')
5
5
  const { version: ddTraceVersion } = require('../../../../package.json')
6
+ const id = require('../../../dd-trace/src/id')
6
7
 
7
8
  const ENCODING_VERSION = 1
8
9
 
10
+ const ALLOWED_CONTENT_TYPES = ['test_session_end', 'test_suite_end', 'test']
11
+
12
+ const TEST_SUITE_KEYS_LENGTH = 11
13
+ const TEST_SESSION_KEYS_LENGTH = 10
14
+
15
+ const CHUNK_SIZE = 4 * 1024 * 1024 // 4MB
16
+
9
17
  function formatSpan (span) {
18
+ let encodingVersion = ENCODING_VERSION
19
+ if (span.type === 'test' && span.meta && span.meta.test_session_id) {
20
+ encodingVersion = 2
21
+ }
10
22
  return {
11
- type: span.type === 'test' ? 'test' : 'span',
12
- version: ENCODING_VERSION,
23
+ type: ALLOWED_CONTENT_TYPES.includes(span.type) ? span.type : 'span',
24
+ version: encodingVersion,
13
25
  content: normalizeSpan(truncateSpan(span))
14
26
  }
15
27
  }
@@ -21,8 +33,8 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
21
33
  this.runtimeId = runtimeId
22
34
  this.service = service
23
35
  this.env = env
24
- this._traceBytes = new Chunk()
25
- this._stringBytes = new Chunk()
36
+ this._traceBytes = new Chunk(CHUNK_SIZE)
37
+ this._stringBytes = new Chunk(CHUNK_SIZE)
26
38
  this._stringCount = 0
27
39
  this._stringMap = {}
28
40
 
@@ -33,8 +45,69 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
33
45
  this.reset()
34
46
  }
35
47
 
48
+ _encodeTestSuite (bytes, content) {
49
+ this._encodeMapPrefix(bytes, TEST_SUITE_KEYS_LENGTH)
50
+ this._encodeString(bytes, 'type')
51
+ this._encodeString(bytes, content.type)
52
+
53
+ this._encodeString(bytes, 'test_session_id')
54
+ this._encodeId(bytes, content.trace_id)
55
+
56
+ this._encodeString(bytes, 'test_suite_id')
57
+ this._encodeId(bytes, content.span_id)
58
+
59
+ this._encodeString(bytes, 'error')
60
+ this._encodeNumber(bytes, content.error)
61
+ this._encodeString(bytes, 'name')
62
+ this._encodeString(bytes, content.name)
63
+ this._encodeString(bytes, 'service')
64
+ this._encodeString(bytes, content.service)
65
+ this._encodeString(bytes, 'resource')
66
+ this._encodeString(bytes, content.resource)
67
+ this._encodeString(bytes, 'start')
68
+ this._encodeNumber(bytes, content.start)
69
+ this._encodeString(bytes, 'duration')
70
+ this._encodeNumber(bytes, content.duration)
71
+ this._encodeString(bytes, 'meta')
72
+ this._encodeMap(bytes, content.meta)
73
+ this._encodeString(bytes, 'metrics')
74
+ this._encodeMap(bytes, content.metrics)
75
+ }
76
+
77
+ _encodeTestSession (bytes, content) {
78
+ this._encodeMapPrefix(bytes, TEST_SESSION_KEYS_LENGTH)
79
+ this._encodeString(bytes, 'type')
80
+ this._encodeString(bytes, content.type)
81
+
82
+ this._encodeString(bytes, 'test_session_id')
83
+ this._encodeId(bytes, content.trace_id)
84
+
85
+ this._encodeString(bytes, 'error')
86
+ this._encodeNumber(bytes, content.error)
87
+ this._encodeString(bytes, 'name')
88
+ this._encodeString(bytes, content.name)
89
+ this._encodeString(bytes, 'service')
90
+ this._encodeString(bytes, content.service)
91
+ this._encodeString(bytes, 'resource')
92
+ this._encodeString(bytes, content.resource)
93
+ this._encodeString(bytes, 'start')
94
+ this._encodeNumber(bytes, content.start)
95
+ this._encodeString(bytes, 'duration')
96
+ this._encodeNumber(bytes, content.duration)
97
+ this._encodeString(bytes, 'meta')
98
+ this._encodeMap(bytes, content.meta)
99
+ this._encodeString(bytes, 'metrics')
100
+ this._encodeMap(bytes, content.metrics)
101
+ }
102
+
36
103
  _encodeEventContent (bytes, content) {
37
- this._encodeMapPrefix(bytes, content)
104
+ const keysLength = Object.keys(content).length
105
+ if (content.meta.test_session_id) {
106
+ this._encodeMapPrefix(bytes, keysLength + 2)
107
+ } else {
108
+ this._encodeMapPrefix(bytes, keysLength)
109
+ }
110
+
38
111
  if (content.type) {
39
112
  this._encodeString(bytes, 'type')
40
113
  this._encodeString(bytes, content.type)
@@ -57,6 +130,24 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
57
130
  this._encodeNumber(bytes, content.start)
58
131
  this._encodeString(bytes, 'duration')
59
132
  this._encodeNumber(bytes, content.duration)
133
+ /**
134
+ * We include `test_session_id` and `test_suite_id`
135
+ * in the root of the event by passing them via the `meta` dict.
136
+ * This is to avoid changing the span format in packages/dd-trace/src/format.js,
137
+ * which can have undesired side effects in other products.
138
+ * But `test_session_id` and `test_suite_id` are *not* supposed to be in `meta`,
139
+ * so we delete them before enconding the dictionary.
140
+ * TODO: find a better way to do this.
141
+ */
142
+ if (content.meta.test_session_id) {
143
+ this._encodeString(bytes, 'test_session_id')
144
+ this._encodeId(bytes, id(content.meta.test_session_id))
145
+ delete content.meta.test_session_id
146
+
147
+ this._encodeString(bytes, 'test_suite_id')
148
+ this._encodeId(bytes, id(content.meta.test_suite_id))
149
+ delete content.meta.test_suite_id
150
+ }
60
151
  this._encodeString(bytes, 'meta')
61
152
  this._encodeMap(bytes, content.meta)
62
153
  this._encodeString(bytes, 'metrics')
@@ -64,7 +155,7 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
64
155
  }
65
156
 
66
157
  _encodeEvent (bytes, event) {
67
- this._encodeMapPrefix(bytes, event)
158
+ this._encodeMapPrefix(bytes, Object.keys(event).length)
68
159
  this._encodeString(bytes, 'type')
69
160
  this._encodeString(bytes, event.type)
70
161
 
@@ -72,7 +163,13 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
72
163
  this._encodeNumber(bytes, event.version)
73
164
 
74
165
  this._encodeString(bytes, 'content')
75
- this._encodeEventContent(bytes, event.content)
166
+ if (event.type === 'span' || event.type === 'test') {
167
+ this._encodeEventContent(bytes, event.content)
168
+ } else if (event.type === 'test_suite_end') {
169
+ this._encodeTestSuite(bytes, event.content)
170
+ } else if (event.type === 'test_session_end') {
171
+ this._encodeTestSession(bytes, event.content)
172
+ }
76
173
  }
77
174
 
78
175
  _encodeNumber (bytes, value) {
@@ -107,18 +204,17 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
107
204
  buffer[offset + 8] = lo
108
205
  }
109
206
 
110
- _encodeMapPrefix (bytes, map) {
111
- const keys = Object.keys(map)
207
+ _encodeMapPrefix (bytes, keysLength) {
112
208
  const buffer = bytes.buffer
113
209
  const offset = bytes.length
114
210
 
115
211
  bytes.reserve(5)
116
212
  bytes.length += 5
117
213
  buffer[offset] = 0xdf
118
- buffer[offset + 1] = keys.length >> 24
119
- buffer[offset + 2] = keys.length >> 16
120
- buffer[offset + 3] = keys.length >> 8
121
- buffer[offset + 4] = keys.length
214
+ buffer[offset + 1] = keysLength >> 24
215
+ buffer[offset + 2] = keysLength >> 16
216
+ buffer[offset + 3] = keysLength >> 8
217
+ buffer[offset + 4] = keysLength
122
218
  }
123
219
 
124
220
  _encode (bytes, trace) {
@@ -171,11 +267,11 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
171
267
  payload.metadata['*']['runtime-id'] = this.runtimeId
172
268
  }
173
269
 
174
- this._encodeMapPrefix(bytes, payload)
270
+ this._encodeMapPrefix(bytes, Object.keys(payload).length)
175
271
  this._encodeString(bytes, 'version')
176
272
  this._encodeNumber(bytes, payload.version)
177
273
  this._encodeString(bytes, 'metadata')
178
- this._encodeMapPrefix(bytes, payload.metadata)
274
+ this._encodeMapPrefix(bytes, Object.keys(payload.metadata).length)
179
275
  this._encodeString(bytes, '*')
180
276
  this._encodeMap(bytes, payload.metadata['*'])
181
277
  this._encodeString(bytes, 'events')
@@ -1,22 +1,32 @@
1
1
  'use strict'
2
2
 
3
+ // TODO: Add test with slow or unresponsive agent.
4
+ // TODO: Add telemetry for things like dropped requests, errors, etc.
5
+
3
6
  const http = require('http')
4
7
  const https = require('https')
5
- const log = require('../../log')
6
8
  const docker = require('./docker')
7
9
  const { storage } = require('../../../../datadog-core')
8
10
 
9
- const httpAgent = new http.Agent({ keepAlive: true })
10
- const httpsAgent = new https.Agent({ keepAlive: true })
11
+ const keepAlive = true
12
+ const maxTotalSockets = 8
13
+ const httpAgent = new http.Agent({ keepAlive, maxTotalSockets })
14
+ const httpsAgent = new https.Agent({ keepAlive, maxTotalSockets })
11
15
  const containerId = docker.id()
12
16
 
17
+ let activeRequests = 0
18
+
13
19
  function request (data, options, keepAlive, callback) {
14
20
  if (!options.headers) {
15
21
  options.headers = {}
16
22
  }
23
+
24
+ // The timeout should be kept low to avoid excessive queueing.
25
+ const timeout = options.timeout || 2000
17
26
  const isSecure = options.protocol === 'https:'
18
27
  const client = isSecure ? https : http
19
28
  const dataArray = [].concat(data)
29
+
20
30
  options.headers['Content-Length'] = byteLength(dataArray)
21
31
 
22
32
  if (containerId) {
@@ -27,39 +37,15 @@ function request (data, options, keepAlive, callback) {
27
37
  options.agent = isSecure ? httpsAgent : httpAgent
28
38
  }
29
39
 
30
- const firstRequest = retriableRequest(options, client, callback)
31
- dataArray.forEach(buffer => firstRequest.write(buffer))
32
-
33
- // The first request will be retried
34
- const firstRequestErrorHandler = () => {
35
- log.debug('Retrying request to the intake')
36
- const retriedReq = retriableRequest(options, client, callback)
37
- dataArray.forEach(buffer => retriedReq.write(buffer))
38
- // The retried request will fail normally
39
- retriedReq.on('error', e => callback(new Error(`Network error trying to reach the intake: ${e.message}`)))
40
- retriedReq.end()
41
- }
42
-
43
- firstRequest.on('error', firstRequestErrorHandler)
44
- firstRequest.end()
45
-
46
- return firstRequest
47
- }
48
-
49
- function retriableRequest (options, client, callback) {
50
- const store = storage.getStore()
51
-
52
- storage.enterWith({ noop: true })
53
-
54
- const timeout = options.timeout || 15000
55
-
56
- const request = client.request(options, res => {
40
+ const onResponse = res => {
57
41
  let responseData = ''
58
42
 
59
43
  res.setTimeout(timeout)
60
44
 
61
45
  res.on('data', chunk => { responseData += chunk })
62
46
  res.on('end', () => {
47
+ activeRequests--
48
+
63
49
  if (res.statusCode >= 200 && res.statusCode <= 299) {
64
50
  callback(null, responseData, res.statusCode)
65
51
  } else {
@@ -69,11 +55,34 @@ function retriableRequest (options, client, callback) {
69
55
  callback(error, null, res.statusCode)
70
56
  }
71
57
  })
72
- })
73
- request.setTimeout(timeout, request.abort)
74
- storage.enterWith(store)
58
+ }
59
+
60
+ const makeRequest = onError => {
61
+ // Limit to 1 request by socket, otherwise drop payload.
62
+ if (activeRequests >= maxTotalSockets) return
63
+
64
+ activeRequests++
65
+
66
+ const store = storage.getStore()
67
+
68
+ storage.enterWith({ noop: true })
69
+
70
+ const req = client.request(options, onResponse)
71
+
72
+ req.once('error', err => {
73
+ activeRequests--
74
+ onError(err)
75
+ })
76
+
77
+ dataArray.forEach(buffer => req.write(buffer))
78
+
79
+ req.setTimeout(timeout, req.abort)
80
+ req.end()
81
+
82
+ storage.enterWith(store)
83
+ }
75
84
 
76
- return request
85
+ makeRequest(() => makeRequest(callback))
77
86
  }
78
87
 
79
88
  function byteLength (data) {
@@ -24,10 +24,6 @@ class NoopTracer extends Tracer {
24
24
  return this._scope
25
25
  }
26
26
 
27
- currentSpan () {
28
- return null
29
- }
30
-
31
27
  getRumData () {
32
28
  return ''
33
29
  }
@@ -36,61 +36,81 @@ const collectDisabledPlugins = () => {
36
36
  // TODO this must always be a singleton.
37
37
  module.exports = class PluginManager {
38
38
  constructor (tracer) {
39
+ this._tracer = tracer
39
40
  this._pluginsByName = {}
40
41
  this._configsByName = {}
41
-
42
- const _disabledPlugins = collectDisabledPlugins()
43
-
44
- for (const PluginClass of Object.values(plugins)) {
45
- /**
46
- * disabling the plugin here instead of in `configure` so we don't waste subscriber
47
- * resources on a plugin that will eventually be disabled anyways
48
- */
49
- if (_disabledPlugins.has(PluginClass.name)) {
50
- log.debug(`Plugin "${PluginClass.name}" was disabled via configuration option.`)
51
- continue
52
- }
53
- if (typeof PluginClass !== 'function') continue
54
- this._pluginsByName[PluginClass.name] = new PluginClass(tracer)
55
- this._configsByName[PluginClass.name] = {}
56
- }
42
+ this._disabledPlugins = collectDisabledPlugins()
57
43
  }
58
44
 
59
45
  // like instrumenter.use()
60
46
  configurePlugin (name, pluginConfig) {
61
- if (!(name in this._pluginsByName)) return
62
47
  if (typeof pluginConfig === 'boolean') {
63
48
  pluginConfig = { enabled: pluginConfig }
64
49
  }
50
+ if (!pluginConfig) {
51
+ pluginConfig = { enabled: true }
52
+ }
65
53
 
66
- const config = {
54
+ this._configsByName[name] = {
67
55
  ...this._configsByName[name],
68
56
  ...pluginConfig
69
57
  }
70
58
 
71
- this._pluginsByName[name].configure(getConfig(name, config))
59
+ if (this._pluginsByName[name]) {
60
+ this._pluginsByName[name].configure(getConfig(name, this._configsByName[name]))
61
+ }
72
62
  }
73
63
 
74
64
  // like instrumenter.enable()
75
- configure (config) {
76
- const { logInjection, serviceMapping } = config
77
-
78
- if (config.plugins !== false) {
79
- for (const name in this._pluginsByName) {
80
- const pluginConfig = {
81
- ...this._configsByName[name],
82
- logInjection
83
- }
84
- if (serviceMapping && serviceMapping[name]) {
85
- pluginConfig.service = serviceMapping[name]
86
- }
87
- this.configurePlugin(name, pluginConfig)
65
+ configure (config = {}) {
66
+ const { logInjection, serviceMapping, experimental, queryStringObfuscation } = config
67
+
68
+ for (const PluginClass of Object.values(plugins)) {
69
+ const name = PluginClass.name
70
+
71
+ if (this._disabledPlugins.has(name)) {
72
+ log.debug(`Plugin "${name}" was disabled via configuration option.`)
73
+ continue
74
+ }
75
+
76
+ if (typeof PluginClass !== 'function') continue
77
+
78
+ this._pluginsByName[name] = new PluginClass(this._tracer)
79
+
80
+ if (config.plugins === false) continue
81
+
82
+ const pluginConfig = {
83
+ ...this._configsByName[name]
84
+ }
85
+
86
+ if (logInjection !== undefined) {
87
+ pluginConfig.logInjection = logInjection
88
+ }
89
+
90
+ if (queryStringObfuscation !== undefined) {
91
+ pluginConfig.queryStringObfuscation = queryStringObfuscation
92
+ }
93
+
94
+ // TODO: update so that it's available for every CI Visibility's plugin
95
+ if (name === 'mocha') {
96
+ pluginConfig.isAgentlessEnabled = experimental && experimental.exporter === 'datadog'
97
+ }
98
+
99
+ if (serviceMapping && serviceMapping[name]) {
100
+ pluginConfig.service = serviceMapping[name]
88
101
  }
102
+
103
+ this.configurePlugin(name, pluginConfig)
89
104
  }
90
105
  }
91
106
 
92
107
  // This is basically just for testing. like intrumenter.disable()
93
108
  destroy () {
94
- for (const name in this._pluginsByName) this._pluginsByName[name].configure({ enabled: false })
109
+ for (const name in this._pluginsByName) {
110
+ this._pluginsByName[name].configure({ enabled: false })
111
+ }
112
+
113
+ this._pluginsByName = {}
114
+ this._configsByName = {}
95
115
  }
96
116
  }
@@ -15,7 +15,6 @@ module.exports = {
15
15
  'express': require('../../../datadog-plugin-express/src'),
16
16
  'fastify': require('../../../datadog-plugin-fastify/src'),
17
17
  'find-my-way': require('../../../datadog-plugin-find-my-way/src'),
18
- 'fs': require('../../../datadog-plugin-fs/src'),
19
18
  'google-cloud-pubsub': require('../../../datadog-plugin-google-cloud-pubsub/src'),
20
19
  'graphql': require('../../../datadog-plugin-graphql/src'),
21
20
  'grpc': require('../../../datadog-plugin-grpc/src'),
@@ -36,6 +36,9 @@ const TEST_IS_RUM_ACTIVE = 'test.is_rum_active'
36
36
  const TEST_CODE_OWNERS = 'test.codeowners'
37
37
  const TEST_SOURCE_FILE = 'test.source.file'
38
38
  const LIBRARY_VERSION = 'library_version'
39
+ const TEST_COMMAND = 'test.command'
40
+ const TEST_SESSION_ID = 'test_session_id'
41
+ const TEST_SUITE_ID = 'test_suite_id'
39
42
 
40
43
  const ERROR_TYPE = 'error.type'
41
44
  const ERROR_MESSAGE = 'error.msg'
@@ -70,7 +73,12 @@ module.exports = {
70
73
  getTestSuitePath,
71
74
  getCodeOwnersFileEntries,
72
75
  getCodeOwnersForFilename,
73
- getTestCommonTags
76
+ getTestCommonTags,
77
+ getTestSessionCommonTags,
78
+ getTestSuiteCommonTags,
79
+ TEST_COMMAND,
80
+ TEST_SESSION_ID,
81
+ TEST_SUITE_ID
74
82
  }
75
83
 
76
84
  function getTestEnvironmentMetadata (testFramework, config) {
@@ -225,3 +233,26 @@ function getCodeOwnersForFilename (filename, entries) {
225
233
  }
226
234
  return null
227
235
  }
236
+
237
+ function getTestSessionCommonTags (command, version) {
238
+ return {
239
+ [SPAN_TYPE]: 'test_session_end',
240
+ [TEST_TYPE]: 'test',
241
+ [RESOURCE_NAME]: `test_session.${command}`,
242
+ [TEST_FRAMEWORK_VERSION]: version,
243
+ [LIBRARY_VERSION]: ddTraceVersion,
244
+ [TEST_COMMAND]: command
245
+ }
246
+ }
247
+
248
+ function getTestSuiteCommonTags (command, version, testSuite) {
249
+ return {
250
+ [SPAN_TYPE]: 'test_suite_end',
251
+ [TEST_TYPE]: 'test',
252
+ [RESOURCE_NAME]: `test_suite.${testSuite}`,
253
+ [TEST_FRAMEWORK_VERSION]: version,
254
+ [LIBRARY_VERSION]: ddTraceVersion,
255
+ [TEST_SUITE]: testSuite,
256
+ [TEST_COMMAND]: command
257
+ }
258
+ }
@@ -43,13 +43,15 @@ const web = {
43
43
  const hooks = getHooks(config)
44
44
  const filter = urlFilter.getFilter(config)
45
45
  const middleware = getMiddlewareSetting(config)
46
+ const queryStringObfuscation = getQsObfuscator(config)
46
47
 
47
48
  return Object.assign({}, config, {
48
49
  headers,
49
50
  validateStatus,
50
51
  hooks,
51
52
  filter,
52
- middleware
53
+ middleware,
54
+ queryStringObfuscation
53
55
  })
54
56
  },
55
57
 
@@ -259,9 +261,9 @@ const web = {
259
261
  const context = contexts.get(req)
260
262
  const span = context.span
261
263
  const error = context.error
262
- const hasMiddlewareError = span.context()._tags['error'] || span.context()._tags['error.msg']
264
+ const hasExistingError = span.context()._tags['error'] || span.context()._tags['error.msg']
263
265
 
264
- if (!hasMiddlewareError && !context.config.validateStatus(statusCode)) {
266
+ if (!hasExistingError && !context.config.validateStatus(statusCode)) {
265
267
  span.setTag(ERROR, error || true)
266
268
  }
267
269
  },
@@ -270,7 +272,10 @@ const web = {
270
272
  addError (req, error) {
271
273
  if (error instanceof Error) {
272
274
  const context = contexts.get(req)
273
- context.error = context.error || error
275
+
276
+ if (context) {
277
+ context.error = error
278
+ }
274
279
  }
275
280
  },
276
281
 
@@ -298,6 +303,40 @@ const web = {
298
303
  context.span.finish()
299
304
  context.finished = true
300
305
  },
306
+
307
+ finishAll (context) {
308
+ const { req, res } = context
309
+
310
+ for (const beforeEnd of context.beforeEnd) {
311
+ beforeEnd()
312
+ }
313
+
314
+ web.finishMiddleware(context)
315
+
316
+ if (incomingHttpRequestEnd.hasSubscribers) {
317
+ incomingHttpRequestEnd.publish({ req, res })
318
+ }
319
+
320
+ web.finishSpan(context)
321
+ },
322
+
323
+ obfuscateQs (config, url) {
324
+ const { queryStringObfuscation } = config
325
+
326
+ if (queryStringObfuscation === false) return url
327
+
328
+ const i = url.indexOf('?')
329
+ if (i === -1) return url
330
+
331
+ const path = url.slice(0, i)
332
+ if (queryStringObfuscation === true) return path
333
+
334
+ let qs = url.slice(i + 1)
335
+
336
+ qs = qs.replace(queryStringObfuscation, '<redacted>')
337
+
338
+ return `${path}?${qs}`
339
+ },
301
340
  wrapWriteHead (context) {
302
341
  const { req, res } = context
303
342
  const writeHead = res.writeHead
@@ -318,21 +357,9 @@ const web = {
318
357
  },
319
358
  wrapRes (context, req, res, end) {
320
359
  return function () {
321
- for (const beforeEnd of context.beforeEnd) {
322
- beforeEnd()
323
- }
324
-
325
- web.finishMiddleware(context)
360
+ web.finishAll(context)
326
361
 
327
- if (incomingHttpRequestEnd.hasSubscribers) {
328
- incomingHttpRequestEnd.publish({ req, res })
329
- }
330
-
331
- const returnValue = end.apply(res, arguments)
332
-
333
- web.finishSpan(context)
334
-
335
- return returnValue
362
+ return end.apply(res, arguments)
336
363
  }
337
364
  },
338
365
  wrapEnd (context) {
@@ -399,11 +426,11 @@ function reactivate (req, fn) {
399
426
  }
400
427
 
401
428
  function addRequestTags (context) {
402
- const { req, span } = context
429
+ const { req, span, config } = context
403
430
  const url = extractURL(req)
404
431
 
405
432
  span.addTags({
406
- [HTTP_URL]: url.split('?')[0],
433
+ [HTTP_URL]: web.obfuscateQs(config, url),
407
434
  [HTTP_METHOD]: req.method,
408
435
  [SPAN_KIND]: SERVER,
409
436
  [SPAN_TYPE]: WEB,
@@ -514,4 +541,30 @@ function getMiddlewareSetting (config) {
514
541
  return true
515
542
  }
516
543
 
544
+ function getQsObfuscator (config) {
545
+ const obfuscator = config.queryStringObfuscation
546
+
547
+ if (typeof obfuscator === 'boolean') {
548
+ return obfuscator
549
+ }
550
+
551
+ if (typeof obfuscator === 'string') {
552
+ if (obfuscator === '') return false // disable obfuscator
553
+
554
+ if (obfuscator === '.*') return true // optimize full redact
555
+
556
+ try {
557
+ return new RegExp(obfuscator, 'gi')
558
+ } catch (err) {
559
+ log.error(err)
560
+ }
561
+ }
562
+
563
+ if (config.hasOwnProperty('queryStringObfuscation')) {
564
+ log.error('Expected `queryStringObfuscation` to be a regex string or boolean.')
565
+ }
566
+
567
+ return true
568
+ }
569
+
517
570
  module.exports = web