dd-trace 2.5.0 → 2.6.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.
@@ -35,6 +35,8 @@ const RESPONSE_HEADERS_PASSLIST = [
35
35
  'content-type'
36
36
  ]
37
37
 
38
+ const metricsQueue = new Map()
39
+
38
40
  function resolveHTTPRequest (context) {
39
41
  if (!context) return {}
40
42
 
@@ -82,6 +84,20 @@ function formatHeaderName (name) {
82
84
  .toLowerCase()
83
85
  }
84
86
 
87
+ function reportMetrics (metrics, store) {
88
+ const req = store && store.get('req')
89
+ const topSpan = web.root(req)
90
+ if (!topSpan) return false
91
+
92
+ if (metrics.duration) {
93
+ topSpan.setTag('_dd.appsec.waf.duration', metrics.duration)
94
+ }
95
+
96
+ if (metrics.rulesVersion) {
97
+ topSpan.setTag('_dd.appsec.event_rules.version', metrics.rulesVersion)
98
+ }
99
+ }
100
+
85
101
  function reportAttack (attackData, store) {
86
102
  const req = store && store.get('req')
87
103
  const topSpan = web.root(req)
@@ -129,9 +145,17 @@ function reportAttack (attackData, store) {
129
145
  topSpan.addTags(newTags)
130
146
  }
131
147
 
132
- function finishAttacks (req, context) {
148
+ function finishRequest (req, context) {
133
149
  const topSpan = web.root(req)
134
- if (!topSpan || !context) return false
150
+ if (!topSpan) return false
151
+
152
+ if (metricsQueue.size) {
153
+ topSpan.addTags(Object.fromEntries(metricsQueue))
154
+
155
+ metricsQueue.clear()
156
+ }
157
+
158
+ if (!context || !topSpan.context()._tags['appsec.event']) return false
135
159
 
136
160
  const resolvedResponse = resolveHTTPResponse(context)
137
161
 
@@ -149,11 +173,13 @@ function setRateLimit (rateLimit) {
149
173
  }
150
174
 
151
175
  module.exports = {
176
+ metricsQueue,
152
177
  resolveHTTPRequest,
153
178
  resolveHTTPResponse,
154
179
  filterHeaders,
155
180
  formatHeaderName,
181
+ reportMetrics,
156
182
  reportAttack,
157
- finishAttacks,
183
+ finishRequest,
158
184
  setRateLimit
159
185
  }
@@ -4,11 +4,11 @@ const callbacks = require('./callbacks')
4
4
 
5
5
  const appliedCallbacks = new Map()
6
6
 
7
- function applyRules (rules) {
7
+ function applyRules (rules, config) {
8
8
  if (appliedCallbacks.has(rules)) return
9
9
 
10
10
  // for now there is only WAF
11
- const callback = new callbacks.DDWAF(rules)
11
+ const callback = new callbacks.DDWAF(rules, config)
12
12
 
13
13
  appliedCallbacks.set(rules, callback)
14
14
  }
@@ -150,10 +150,29 @@ class Config {
150
150
  path.join(__dirname, 'appsec', 'recommended.json')
151
151
  )
152
152
  const DD_APPSEC_TRACE_RATE_LIMIT = coalesce(
153
- appsec.rateLimit,
154
- process.env.DD_APPSEC_TRACE_RATE_LIMIT,
153
+ parseInt(appsec.rateLimit),
154
+ parseInt(process.env.DD_APPSEC_TRACE_RATE_LIMIT),
155
155
  100
156
156
  )
157
+ const DD_APPSEC_WAF_TIMEOUT = coalesce(
158
+ parseInt(appsec.wafTimeout),
159
+ parseInt(process.env.DD_APPSEC_WAF_TIMEOUT),
160
+ 5e3 // µs
161
+ )
162
+ const DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP = coalesce(
163
+ appsec.obfuscatorKeyRegex,
164
+ process.env.DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP,
165
+ `(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?)key)|token|consumer_?(?:id|key|se\
166
+ cret)|sign(?:ed|ature)|bearer|authorization`
167
+ )
168
+ const DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP = coalesce(
169
+ appsec.obfuscatorValueRegex,
170
+ process.env.DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP,
171
+ `(?i)(?:p(?:ass)?w(?:or)?d|pass(?:_?phrase)?|secret|(?:api_?|private_?|public_?|access_?|secret_?)key(?:_?id)?|to\
172
+ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)(?:\\s*=[^;]|"\\s*:\\s*"[^"]+")|bearer\
173
+ \\s+[a-z0-9\\._\\-]+|token:[a-z0-9]{13}|gh[opsu]_[0-9a-zA-Z]{36}|ey[I-L][\\w=-]+\\.ey[I-L][\\w=-]+(?:\\.[\\w.+\\/=-]+)?\
174
+ |[\\-]{5}BEGIN[a-z\\s]+PRIVATE\\sKEY[\\-]{5}[^\\-]+[\\-]{5}END[a-z\\s]+PRIVATE\\sKEY|ssh-rsa\\s*[a-z0-9\\/\\.+]{100,}`
175
+ )
157
176
 
158
177
  const sampler = (options.experimental && options.experimental.sampler) || {}
159
178
  const ingestion = options.ingestion || {}
@@ -223,7 +242,10 @@ class Config {
223
242
  this.appsec = {
224
243
  enabled: isTrue(DD_APPSEC_ENABLED),
225
244
  rules: DD_APPSEC_RULES,
226
- rateLimit: DD_APPSEC_TRACE_RATE_LIMIT
245
+ rateLimit: DD_APPSEC_TRACE_RATE_LIMIT,
246
+ wafTimeout: DD_APPSEC_WAF_TIMEOUT,
247
+ obfuscatorKeyRegex: DD_APPSEC_OBFUSCATION_PARAMETER_KEY_REGEXP,
248
+ obfuscatorValueRegex: DD_APPSEC_OBFUSCATION_PARAMETER_VALUE_REGEXP
227
249
  }
228
250
 
229
251
  tagger.add(this.tags, {
@@ -22,7 +22,6 @@ const map = {
22
22
  function format (span) {
23
23
  const formatted = formatSpan(span)
24
24
 
25
- extractError(formatted, span)
26
25
  extractRootTags(formatted, span)
27
26
  extractChunkTags(formatted, span)
28
27
  extractTags(formatted, span)
@@ -74,8 +73,8 @@ function extractTags (trace, span) {
74
73
  addTag({}, trace.metrics, tag, tags[tag] === undefined || tags[tag] ? 1 : 0)
75
74
  break
76
75
  case 'error':
77
- if (tags[tag] && (context._name !== 'fs.operation')) {
78
- trace.error = 1
76
+ if (context._name !== 'fs.operation') {
77
+ extractError(trace, tags[tag])
79
78
  }
80
79
  break
81
80
  case 'error.type':
@@ -84,6 +83,8 @@ function extractTags (trace, span) {
84
83
  // HACK: remove when implemented in the backend
85
84
  if (context._name !== 'fs.operation') {
86
85
  trace.error = 1
86
+ } else {
87
+ break
87
88
  }
88
89
  default: // eslint-disable-line no-fallthrough
89
90
  addTag(trace.meta, trace.metrics, tag, tags[tag])
@@ -122,8 +123,11 @@ function extractChunkTags (trace, span) {
122
123
  }
123
124
  }
124
125
 
125
- function extractError (trace, span) {
126
- const error = span.context()._tags['error']
126
+ function extractError (trace, error) {
127
+ if (!error) return
128
+
129
+ trace.error = 1
130
+
127
131
  if (isError(error)) {
128
132
  addTag(trace.meta, trace.metrics, 'error.msg', error.message)
129
133
  addTag(trace.meta, trace.metrics, 'error.type', error.name)
@@ -19,6 +19,10 @@ const {
19
19
  } = require('./tags')
20
20
  const id = require('../../id')
21
21
 
22
+ const { SPAN_TYPE, RESOURCE_NAME, SAMPLING_PRIORITY } = require('../../../../../ext/tags')
23
+ const { SAMPLING_RULE_DECISION } = require('../../constants')
24
+ const { AUTO_KEEP } = require('../../../../../ext/priority')
25
+
22
26
  const TEST_FRAMEWORK = 'test.framework'
23
27
  const TEST_FRAMEWORK_VERSION = 'test.framework_version'
24
28
  const TEST_TYPE = 'test.type'
@@ -60,7 +64,8 @@ module.exports = {
60
64
  getTestParentSpan,
61
65
  getTestSuitePath,
62
66
  getCodeOwnersFileEntries,
63
- getCodeOwnersForFilename
67
+ getCodeOwnersForFilename,
68
+ getTestCommonTags
64
69
  }
65
70
 
66
71
  function getTestEnvironmentMetadata (testFramework, config) {
@@ -134,6 +139,20 @@ function getTestParentSpan (tracer) {
134
139
  'x-datadog-parent-id': '0000000000000000'
135
140
  })
136
141
  }
142
+
143
+ function getTestCommonTags (name, suite, version) {
144
+ return {
145
+ [SPAN_TYPE]: 'test',
146
+ [TEST_TYPE]: 'test',
147
+ [SAMPLING_RULE_DECISION]: 1,
148
+ [SAMPLING_PRIORITY]: AUTO_KEEP,
149
+ [TEST_NAME]: name,
150
+ [TEST_SUITE]: suite,
151
+ [RESOURCE_NAME]: `${suite}.${name}`,
152
+ [TEST_FRAMEWORK_VERSION]: version
153
+ }
154
+ }
155
+
137
156
  /**
138
157
  * We want to make sure that test suites are reported the same way for
139
158
  * every OS, so we replace `path.sep` by `/`
@@ -69,6 +69,17 @@ const web = {
69
69
  context.span = span
70
70
  context.res = res
71
71
 
72
+ if (!config.filter(req.url)) {
73
+ span.setTag(MANUAL_DROP, true)
74
+ span.context()._trace.isRecording = false
75
+ }
76
+
77
+ if (config.service) {
78
+ span.setTag(SERVICE_NAME, config.service)
79
+ }
80
+
81
+ analyticsSampler.sample(span, config.measured, true)
82
+
72
83
  return span
73
84
  },
74
85
  wrap (req) {
@@ -83,16 +94,6 @@ const web = {
83
94
  instrument (tracer, config, req, res, name, callback) {
84
95
  const span = this.startSpan(tracer, config, req, res, name)
85
96
 
86
- if (!config.filter(req.url)) {
87
- span.setTag(MANUAL_DROP, true)
88
- }
89
-
90
- if (config.service) {
91
- span.setTag(SERVICE_NAME, config.service)
92
- }
93
-
94
- analyticsSampler.sample(span, config.measured, true)
95
-
96
97
  this.wrap(req)
97
98
 
98
99
  return callback && tracer.scope().activate(span, () => callback(span))
@@ -3,7 +3,7 @@
3
3
  class NativeCpuProfiler {
4
4
  constructor (options = {}) {
5
5
  this.type = 'wall'
6
- this._samplingInterval = options.samplingInterval || 10 * 1000
6
+ this._samplingInterval = options.samplingInterval || 1e6 / 99 // 99hz
7
7
  this._mapper = undefined
8
8
  this._pprof = undefined
9
9
  }
@@ -32,7 +32,10 @@ class SpanProcessor {
32
32
  }
33
33
  }
34
34
 
35
- this._exporter.export(formatted)
35
+ if (formatted.length !== 0 && trace.isRecording !== false) {
36
+ this._exporter.export(formatted)
37
+ }
38
+
36
39
  this._erase(trace, active)
37
40
  }
38
41
  }
@@ -186,6 +186,7 @@ const requirePackageJson = require('${requirePackageJsonPath}')
186
186
 
187
187
  module.exports = {
188
188
  get (id) { return require(id || '${name}') },
189
+ getPath (id) { return require.resolve(id || '${name}' ) },
189
190
  version () { return requirePackageJson('${name}', module).version }
190
191
  }
191
192
  `
@@ -1,272 +0,0 @@
1
- const { promisify } = require('util')
2
-
3
- const { RESOURCE_NAME } = require('../../../ext/tags')
4
- const {
5
- TEST_NAME,
6
- TEST_SUITE,
7
- TEST_STATUS,
8
- TEST_FRAMEWORK_VERSION,
9
- JEST_TEST_RUNNER,
10
- ERROR_MESSAGE,
11
- ERROR_TYPE,
12
- TEST_PARAMETERS,
13
- CI_APP_ORIGIN,
14
- getTestEnvironmentMetadata,
15
- getTestParametersString,
16
- finishAllTraceSpans,
17
- getTestSuitePath
18
- } = require('../../dd-trace/src/plugins/util/test')
19
- const {
20
- getFormattedJestTestParameters,
21
- getTestSpanTags,
22
- setSuppressedErrors
23
- } = require('./util')
24
-
25
- const originals = new WeakMap()
26
-
27
- function getVmContext (environment) {
28
- if (typeof environment.getVmContext === 'function') {
29
- return environment.getVmContext()
30
- }
31
- return null
32
- }
33
-
34
- function wrapEnvironment (BaseEnvironment) {
35
- return class DatadogJestEnvironment extends BaseEnvironment {
36
- constructor (config, context) {
37
- super(config, context)
38
- this.testSuite = getTestSuitePath(context.testPath, config.rootDir)
39
- this.testSpansByTestName = {}
40
- this.originalTestFnByTestName = {}
41
- }
42
- }
43
- }
44
-
45
- function createWrapTeardown (tracer, instrumenter) {
46
- return function wrapTeardown (teardown) {
47
- return async function teardownWithTrace () {
48
- instrumenter.unwrap(this.global.test, 'each')
49
- nameToParams = {}
50
- // for jest-jasmine2
51
- if (this.global.jasmine) {
52
- instrumenter.unwrap(this.global.jasmine.Spec.prototype, 'onException')
53
- instrumenter.unwrap(this.global, 'it')
54
- instrumenter.unwrap(this.global, 'fit')
55
- instrumenter.unwrap(this.global, 'xit')
56
- }
57
-
58
- instrumenter.unwrap(this.global.test, 'each')
59
-
60
- return teardown.apply(this, arguments).finally(() => {
61
- return new Promise(resolve => tracer._exporter._writer.flush(resolve))
62
- })
63
- }
64
- }
65
- }
66
-
67
- let nameToParams = {}
68
-
69
- const isTimeout = (event) => {
70
- return event.error &&
71
- typeof event.error === 'string' &&
72
- event.error.startsWith('Exceeded timeout')
73
- }
74
-
75
- function createHandleTestEvent (tracer, testEnvironmentMetadata, instrumenter) {
76
- return async function handleTestEventWithTrace (event) {
77
- if (event.name === 'test_retry') {
78
- let testName = event.test && event.test.name
79
- const context = getVmContext(this)
80
- if (context) {
81
- const { currentTestName } = context.expect.getState()
82
- testName = currentTestName
83
- }
84
- // If it's a retry, we restore the original test function so that it is not wrapped again
85
- if (this.originalTestFnByTestName[testName]) {
86
- event.test.fn = this.originalTestFnByTestName[testName]
87
- }
88
- return
89
- }
90
- if (event.name === 'test_fn_failure') {
91
- if (!isTimeout(event)) {
92
- return
93
- }
94
- const context = getVmContext(this)
95
- if (context) {
96
- const { currentTestName } = context.expect.getState()
97
- const testSpan = this.testSpansByTestName[`${currentTestName}_${event.test.invocations}`]
98
- if (testSpan) {
99
- testSpan.setTag(ERROR_TYPE, 'Timeout')
100
- testSpan.setTag(ERROR_MESSAGE, event.error)
101
- testSpan.setTag(TEST_STATUS, 'fail')
102
- }
103
- }
104
- return
105
- }
106
- if (event.name === 'setup') {
107
- instrumenter.wrap(this.global.test, 'each', function (original) {
108
- return function () {
109
- const testParameters = getFormattedJestTestParameters(arguments)
110
- const eachBind = original.apply(this, arguments)
111
- return function () {
112
- const [testName] = arguments
113
- nameToParams[testName] = testParameters
114
- return eachBind.apply(this, arguments)
115
- }
116
- }
117
- })
118
- return
119
- }
120
-
121
- if (event.name !== 'test_skip' &&
122
- event.name !== 'test_todo' &&
123
- event.name !== 'test_start' &&
124
- event.name !== 'hook_failure') {
125
- return
126
- }
127
- // for hook_failure events the test entry might not be defined, because the hook
128
- // is not necessarily associated to a test:
129
- if (!event.test) {
130
- return
131
- }
132
-
133
- const { childOf, commonSpanTags } = getTestSpanTags(tracer, testEnvironmentMetadata)
134
-
135
- let testName = event.test.name
136
- const context = getVmContext(this)
137
-
138
- if (context) {
139
- const { currentTestName } = context.expect.getState()
140
- testName = currentTestName
141
- }
142
- const spanTags = {
143
- ...commonSpanTags,
144
- [TEST_NAME]: testName,
145
- [TEST_SUITE]: this.testSuite,
146
- [TEST_FRAMEWORK_VERSION]: tracer._version,
147
- [JEST_TEST_RUNNER]: 'jest-circus'
148
- }
149
-
150
- const testParametersString = getTestParametersString(nameToParams, event.test.name)
151
- if (testParametersString) {
152
- spanTags[TEST_PARAMETERS] = testParametersString
153
- }
154
-
155
- const resource = `${this.testSuite}.${testName}`
156
- if (event.name === 'test_skip' || event.name === 'test_todo') {
157
- const testSpan = tracer.startSpan(
158
- 'jest.test',
159
- {
160
- childOf,
161
- tags: {
162
- ...spanTags,
163
- [RESOURCE_NAME]: resource,
164
- [TEST_STATUS]: 'skip'
165
- }
166
- }
167
- )
168
- testSpan.context()._trace.origin = CI_APP_ORIGIN
169
- testSpan.finish()
170
- return
171
- }
172
- if (event.name === 'hook_failure') {
173
- const testSpan = tracer.startSpan(
174
- 'jest.test',
175
- {
176
- childOf,
177
- tags: {
178
- ...spanTags,
179
- [RESOURCE_NAME]: resource,
180
- [TEST_STATUS]: 'fail'
181
- }
182
- }
183
- )
184
- testSpan.context()._trace.origin = CI_APP_ORIGIN
185
- if (event.test.errors && event.test.errors.length) {
186
- const error = new Error(event.test.errors[0][0])
187
- error.stack = event.test.errors[0][1].stack
188
- testSpan.setTag('error', error)
189
- }
190
- testSpan.finish()
191
- return
192
- }
193
- // event.name === test_start at this point
194
- const environment = this
195
- environment.originalTestFnByTestName[testName] = event.test.fn
196
-
197
- let specFunction = event.test.fn
198
- if (specFunction.length) {
199
- specFunction = promisify(specFunction)
200
- }
201
- event.test.fn = tracer.wrap(
202
- 'jest.test',
203
- {
204
- type: 'test',
205
- childOf,
206
- resource,
207
- tags: spanTags
208
- },
209
- async () => {
210
- let result
211
- const testSpan = tracer.scope().active()
212
- environment.testSpansByTestName[`${testName}_${event.test.invocations}`] = testSpan
213
- testSpan.context()._trace.origin = CI_APP_ORIGIN
214
- try {
215
- result = await specFunction()
216
- // it may have been set already if the test timed out
217
- let suppressedErrors = []
218
- const context = getVmContext(environment)
219
- if (context) {
220
- suppressedErrors = context.expect.getState().suppressedErrors
221
- }
222
- setSuppressedErrors(suppressedErrors, testSpan)
223
- if (!testSpan._spanContext._tags[TEST_STATUS]) {
224
- testSpan.setTag(TEST_STATUS, 'pass')
225
- }
226
- } catch (error) {
227
- testSpan.setTag(TEST_STATUS, 'fail')
228
- testSpan.setTag('error', error)
229
- throw error
230
- } finally {
231
- finishAllTraceSpans(testSpan)
232
- }
233
- return result
234
- }
235
- )
236
- }
237
- }
238
-
239
- function patch (Environment, tracer, config) {
240
- const testEnvironmentMetadata = getTestEnvironmentMetadata('jest', config)
241
- const proto = Environment.prototype
242
-
243
- this.wrap(proto, 'teardown', createWrapTeardown(tracer, this))
244
-
245
- const newHandleTestEvent = createHandleTestEvent(tracer, testEnvironmentMetadata, this)
246
- originals.set(newHandleTestEvent, proto.handleTestEvent)
247
- proto.handleTestEvent = newHandleTestEvent
248
-
249
- return wrapEnvironment(Environment)
250
- }
251
-
252
- function unpatch (Environment) {
253
- const proto = Environment.prototype
254
-
255
- this.unwrap(Environment.prototype, 'teardown')
256
- proto.handleTestEvent = originals.get(proto.handleTestEvent)
257
- }
258
-
259
- module.exports = [
260
- {
261
- name: 'jest-environment-node',
262
- versions: ['>=24.8.0'],
263
- patch,
264
- unpatch
265
- },
266
- {
267
- name: 'jest-environment-jsdom',
268
- versions: ['>=24.8.0'],
269
- patch,
270
- unpatch
271
- }
272
- ]