dd-trace 2.15.0 → 2.16.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 (27) hide show
  1. package/LICENSE-3rdparty.csv +2 -1
  2. package/README.md +4 -0
  3. package/package.json +5 -4
  4. package/packages/datadog-instrumentations/src/crypto.js +14 -12
  5. package/packages/datadog-instrumentations/src/grpc/server.js +15 -7
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  7. package/packages/datadog-instrumentations/src/jest.js +136 -14
  8. package/packages/datadog-instrumentations/src/mocha.js +77 -31
  9. package/packages/datadog-instrumentations/src/next.js +7 -3
  10. package/packages/datadog-plugin-jest/src/index.js +106 -6
  11. package/packages/datadog-plugin-mocha/src/index.js +15 -7
  12. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  13. package/packages/dd-trace/src/appsec/iast/analyzers/weak-cipher-analyzer.js +27 -0
  14. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +13 -5
  15. package/packages/dd-trace/src/appsec/recommended.json +1144 -275
  16. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
  17. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -3
  18. package/packages/dd-trace/src/config.js +15 -1
  19. package/packages/dd-trace/src/encode/0.4.js +7 -1
  20. package/packages/dd-trace/src/encode/0.5.js +7 -1
  21. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +2 -2
  22. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +32 -20
  23. package/packages/dd-trace/src/exporters/common/request.js +3 -3
  24. package/packages/dd-trace/src/plugins/index.js +3 -0
  25. package/packages/dd-trace/src/plugins/util/ip_blocklist.js +30 -4
  26. package/packages/dd-trace/src/plugins/util/redis.js +0 -74
  27. package/packages/dd-trace/src/plugins/util/tx.js +0 -75
@@ -4,11 +4,11 @@ require,@datadog/native-metrics,Apache license 2.0,Copyright 2018 Datadog Inc.
4
4
  require,@datadog/pprof,Apache license 2.0,Copyright 2019 Google Inc.
5
5
  require,@datadog/sketches-js,Apache license 2.0,Copyright 2020 Datadog Inc.
6
6
  require,@types/node,MIT,Copyright Authors
7
- require,cidr-matcher,MIT,Copyright 2015 Marco Pracucci
8
7
  require,crypto-randomuuid,MIT,Copyright 2021 Node.js Foundation and contributors
9
8
  require,diagnostics_channel,MIT,Copyright 2021 Simon D.
10
9
  require,ignore,MIT,Copyright 2013 Kael Zhang and contributors
11
10
  require,import-in-the-middle,Apache license 2.0,Copyright 2021 Datadog Inc.
11
+ require,ipaddr.js,MIT,Copyright 2011-2017 whitequark
12
12
  require,istanbul-lib-coverage,BSD-3-Clause,Copyright 2012-2015 Yahoo! Inc.
13
13
  require,koalas,MIT,Copyright 2013-2017 Brian Woodward
14
14
  require,limiter,MIT,Copyright 2011 John Hurliman
@@ -16,6 +16,7 @@ require,lodash.kebabcase,MIT,Copyright JS Foundation and other contributors
16
16
  require,lodash.pick,MIT,Copyright JS Foundation and other contributors
17
17
  require,lodash.sortby,MIT,Copyright JS Foundation and other contributors
18
18
  require,lodash.uniq,MIT,Copyright JS Foundation and other contributors
19
+ require,lru-cache,ISC,Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors
19
20
  require,methods,MIT,Copyright 2013-2014 TJ Holowaychuk
20
21
  require,module-details-from-path,MIT,Copyright 2016 Thomas Watson Steen
21
22
  require,opentracing,MIT,Copyright 2016 Resonance Labs Inc
package/README.md CHANGED
@@ -21,6 +21,10 @@ For descriptions of terminology used in APM, take a look at the [official docume
21
21
 
22
22
  Before contributing to this open source project, read our [CONTRIBUTING.md](https://github.com/DataDog/dd-trace-js/blob/master/CONTRIBUTING.md).
23
23
 
24
+ ## Security Vulnerabilities
25
+
26
+ If you have found a security issue, please contact the security team directly at [security@datadoghq.com](mailto:security@datadoghq.com).
27
+
24
28
  ### Requirements
25
29
 
26
30
  Since this project supports multiple Node versions, using a version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -59,15 +59,15 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@datadog/native-appsec": "^1.2.1",
62
- "@datadog/native-metrics": "^1.4.2",
62
+ "@datadog/native-metrics": "^1.4.3",
63
63
  "@datadog/pprof": "^1.0.2",
64
64
  "@datadog/sketches-js": "^2.1.0",
65
65
  "@types/node": ">=12",
66
- "cidr-matcher": "^2.1.1",
67
66
  "crypto-randomuuid": "^1.0.0",
68
67
  "diagnostics_channel": "^1.1.0",
69
68
  "ignore": "^5.2.0",
70
- "import-in-the-middle": "^1.3.1",
69
+ "import-in-the-middle": "^1.3.3",
70
+ "ipaddr.js": "^2.0.1",
71
71
  "istanbul-lib-coverage": "3.2.0",
72
72
  "koalas": "^1.0.2",
73
73
  "limiter": "^1.1.4",
@@ -75,6 +75,7 @@
75
75
  "lodash.pick": "^4.4.0",
76
76
  "lodash.sortby": "^4.7.0",
77
77
  "lodash.uniq": "^4.5.0",
78
+ "lru-cache": "^7.14.0",
78
79
  "methods": "^1.1.2",
79
80
  "module-details-from-path": "^1.0.3",
80
81
  "opentracing": ">=0.12.1",
@@ -6,25 +6,27 @@ const {
6
6
  } = require('./helpers/instrument')
7
7
  const shimmer = require('../../datadog-shimmer')
8
8
 
9
- const cryptoCh = channel('datadog:crypto:hashing:start')
9
+ const cryptoHashCh = channel('datadog:crypto:hashing:start')
10
+ const cryptoCipherCh = channel('datadog:crypto:cipher:start')
11
+
12
+ const hashMethods = ['createHash', 'createHmac', 'createSign', 'createVerify', 'sign', 'verify']
13
+ const cipherMethods = ['createCipheriv', 'createDecipheriv']
10
14
 
11
15
  addHook({ name: 'crypto' }, crypto => {
12
- shimmer.massWrap(
13
- crypto,
14
- ['createHash', 'createHmac', 'createSign', 'createVerify', 'sign', 'verify'],
15
- wrapMethod
16
- )
16
+ shimmer.massWrap(crypto, hashMethods, wrapCryptoMethod(cryptoHashCh))
17
+ shimmer.massWrap(crypto, cipherMethods, wrapCryptoMethod(cryptoCipherCh))
17
18
  return crypto
18
19
  })
19
20
 
20
- function wrapMethod (cryptoMethod) {
21
- return function () {
22
- if (cryptoCh.hasSubscribers) {
23
- if (arguments.length > 0) {
21
+ function wrapCryptoMethod (channel) {
22
+ function wrapMethod (cryptoMethod) {
23
+ return function () {
24
+ if (channel.hasSubscribers && arguments.length > 0) {
24
25
  const algorithm = arguments[0]
25
- cryptoCh.publish({ algorithm })
26
+ channel.publish({ algorithm })
26
27
  }
28
+ return cryptoMethod.apply(this, arguments)
27
29
  }
28
- return cryptoMethod.apply(this, arguments)
29
30
  }
31
+ return wrapMethod
30
32
  }
@@ -37,15 +37,17 @@ function wrapHandler (func, name) {
37
37
  return requestResource.runInAsyncScope(() => {
38
38
  startChannel.publish({ name, metadata, type })
39
39
 
40
- // Finish the span if the call was cancelled.
41
- call.once('cancelled', requestResource.bind(() => {
40
+ const onCancel = requestResource.bind(() => {
42
41
  finishChannel.publish({ code: CANCELLED })
43
- }))
42
+ })
43
+
44
+ // Finish the span if the call was cancelled.
45
+ call.once('cancelled', onCancel)
44
46
 
45
47
  if (isStream) {
46
- wrapStream(call, requestResource, parentResource)
48
+ wrapStream(call, requestResource, onCancel)
47
49
  } else {
48
- arguments[1] = wrapCallback(callback, requestResource, parentResource)
50
+ arguments[1] = wrapCallback(callback, call, requestResource, parentResource, onCancel)
49
51
  }
50
52
 
51
53
  shimmer.wrap(call, 'emit', emit => requestResource.bind(emit))
@@ -65,7 +67,7 @@ function wrapRegister (register) {
65
67
  }
66
68
  }
67
69
 
68
- function wrapStream (call, requestResource) {
70
+ function wrapStream (call, requestResource, onCancel) {
69
71
  if (call.call && call.call.sendStatus) {
70
72
  call.call.sendStatus = wrapSendStatus(call.call.sendStatus, requestResource)
71
73
  }
@@ -77,6 +79,8 @@ function wrapStream (call, requestResource) {
77
79
  errorChannel.publish(args[0])
78
80
  finishChannel.publish({ code: args[0].code })
79
81
 
82
+ call.removeListener('cancelled', onCancel)
83
+
80
84
  break
81
85
 
82
86
  // Finish the span of the response only if it was successful.
@@ -90,6 +94,8 @@ function wrapStream (call, requestResource) {
90
94
  finishChannel.publish()
91
95
  }
92
96
 
97
+ call.removeListener('cancelled', onCancel)
98
+
93
99
  break
94
100
  }
95
101
 
@@ -98,7 +104,7 @@ function wrapStream (call, requestResource) {
98
104
  })
99
105
  }
100
106
 
101
- function wrapCallback (callback, requestResource, parentResource) {
107
+ function wrapCallback (callback, call, requestResource, parentResource, onCancel) {
102
108
  return function (err, value, trailer, flags) {
103
109
  requestResource.runInAsyncScope(() => {
104
110
  if (err instanceof Error) {
@@ -107,6 +113,8 @@ function wrapCallback (callback, requestResource, parentResource) {
107
113
  } else {
108
114
  finishChannel.publish({ code: OK, trailer })
109
115
  }
116
+
117
+ call.removeListener('cancelled', onCancel)
110
118
  })
111
119
 
112
120
  if (callback) {
@@ -7,6 +7,7 @@ module.exports = {
7
7
  '@google-cloud/pubsub': () => require('../google-cloud-pubsub'),
8
8
  '@grpc/grpc-js': () => require('../grpc'),
9
9
  '@hapi/hapi': () => require('../hapi'),
10
+ '@jest/core': () => require('../jest'),
10
11
  '@koa/router': () => require('../koa'),
11
12
  '@node-redis/client': () => require('../redis'),
12
13
  'amqp10': () => require('../amqp10'),
@@ -32,6 +33,8 @@ module.exports = {
32
33
  'http2': () => require('../http2'),
33
34
  'https': () => require('../http'),
34
35
  'ioredis': () => require('../ioredis'),
36
+ 'jest-circus': () => require('../jest'),
37
+ 'jest-config': () => require('../jest'),
35
38
  'jest-environment-node': () => require('../jest'),
36
39
  'jest-environment-jsdom': () => require('../jest'),
37
40
  'jest-jasmine2': () => require('../jest'),
@@ -3,13 +3,20 @@ const istanbul = require('istanbul-lib-coverage')
3
3
  const { addHook, channel, AsyncResource } = require('./helpers/instrument')
4
4
  const shimmer = require('../../datadog-shimmer')
5
5
 
6
+ const testSessionStartCh = channel('ci:jest:session:start')
7
+ const testSessionFinishCh = channel('ci:jest:session:finish')
8
+
9
+ const testSessionConfigurationCh = channel('ci:jest:session:configuration')
10
+
11
+ const testSuiteStartCh = channel('ci:jest:test-suite:start')
12
+ const testSuiteFinishCh = channel('ci:jest:test-suite:finish')
13
+ const testSuiteCodeCoverageCh = channel('ci:jest:test-suite:code-coverage')
14
+
6
15
  const testStartCh = channel('ci:jest:test:start')
7
16
  const testSkippedCh = channel('ci:jest:test:skip')
8
17
  const testRunFinishCh = channel('ci:jest:test:finish')
9
18
  const testErrCh = channel('ci:jest:test:err')
10
19
 
11
- const testCodeCoverageCh = channel('ci:jest:test:code-coverage')
12
-
13
20
  const {
14
21
  getTestSuitePath,
15
22
  getTestParametersString
@@ -17,7 +24,8 @@ const {
17
24
 
18
25
  const { getFormattedJestTestParameters, getJestTestName } = require('../../datadog-plugin-jest/src/util')
19
26
 
20
- // This function also resets the coverage counters
27
+ const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
28
+
21
29
  function extractCoverageInformation (coverage, rootDir) {
22
30
  const coverageMap = istanbul.createCoverageMap(coverage)
23
31
 
@@ -28,8 +36,6 @@ function extractCoverageInformation (coverage, rootDir) {
28
36
  const lineCoverage = fileCoverage.getLineCoverage()
29
37
  const isAnyLineExecuted = Object.entries(lineCoverage).some(([, numExecutions]) => !!numExecutions)
30
38
 
31
- fileCoverage.resetHits()
32
-
33
39
  return isAnyLineExecuted
34
40
  })
35
41
  .map(filename => filename.replace(`${rootDir}/`, ''))
@@ -63,6 +69,16 @@ function formatJestError (errors) {
63
69
  return error
64
70
  }
65
71
 
72
+ function getTestEnvironmentOptions (config) {
73
+ if (config.projectConfig && config.projectConfig.testEnvironmentOptions) { // newer versions
74
+ return config.projectConfig.testEnvironmentOptions
75
+ }
76
+ if (config.testEnvironmentOptions) {
77
+ return config.testEnvironmentOptions
78
+ }
79
+ return {}
80
+ }
81
+
66
82
  function getWrappedEnvironment (BaseEnvironment) {
67
83
  return class DatadogEnvironment extends BaseEnvironment {
68
84
  constructor (config, context) {
@@ -72,6 +88,8 @@ function getWrappedEnvironment (BaseEnvironment) {
72
88
  this.testSuite = getTestSuitePath(context.testPath, rootDir)
73
89
  this.nameToParams = {}
74
90
  this.global._ddtrace = global._ddtrace
91
+
92
+ this.testEnvironmentOptions = getTestEnvironmentOptions(config)
75
93
  }
76
94
 
77
95
  async handleTestEvent (event, state) {
@@ -114,10 +132,6 @@ function getWrappedEnvironment (BaseEnvironment) {
114
132
  if (event.name === 'test_done') {
115
133
  const asyncResource = asyncResources.get(event.test)
116
134
  asyncResource.runInAsyncScope(() => {
117
- if (this.global.__coverage__) {
118
- const coverageFiles = extractCoverageInformation(this.global.__coverage__, this.rootDir)
119
- testCodeCoverageCh.publish(coverageFiles)
120
- }
121
135
  let status = 'pass'
122
136
  if (event.test.errors && event.test.errors.length) {
123
137
  status = 'fail'
@@ -163,11 +177,113 @@ addHook({
163
177
  versions: ['>=24.8.0']
164
178
  }, getTestEnvironment)
165
179
 
180
+ function cliWrapper (cli) {
181
+ const wrapped = shimmer.wrap(cli, 'runCLI', runCLI => async function () {
182
+ const processArgv = process.argv.slice(2).join(' ')
183
+ sessionAsyncResource.runInAsyncScope(() => {
184
+ testSessionStartCh.publish(`jest ${processArgv}`)
185
+ })
186
+ return runCLI.apply(this, arguments).then(result => {
187
+ const { results: { success } } = result
188
+ sessionAsyncResource.runInAsyncScope(() => {
189
+ testSessionFinishCh.publish(success ? 'pass' : 'fail')
190
+ })
191
+ return result
192
+ })
193
+ })
194
+
195
+ cli.runCLI = wrapped.runCLI
196
+
197
+ return cli
198
+ }
199
+
166
200
  addHook({
167
- name: 'jest-jasmine2',
168
- versions: ['>=24.8.0'],
169
- file: 'build/jasmineAsyncInstall.js'
170
- }, (jasmineAsyncInstallExport) => {
201
+ name: '@jest/core',
202
+ file: 'build/cli/index.js',
203
+ versions: ['>=24.8.0']
204
+ }, cliWrapper)
205
+
206
+ function jestAdapterWrapper (jestAdapter) {
207
+ const adapter = jestAdapter.default ? jestAdapter.default : jestAdapter
208
+ const newAdapter = shimmer.wrap(adapter, function () {
209
+ const environment = arguments[2]
210
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
211
+ return asyncResource.runInAsyncScope(() => {
212
+ testSuiteStartCh.publish({
213
+ testSuite: environment.testSuite,
214
+ testEnvironmentOptions: environment.testEnvironmentOptions
215
+ })
216
+ return adapter.apply(this, arguments).then(suiteResults => {
217
+ const { numFailingTests, skipped, failureMessage: errorMessage } = suiteResults
218
+ let status = 'pass'
219
+ if (skipped) {
220
+ status = 'skipped'
221
+ } else if (numFailingTests !== 0) {
222
+ status = 'fail'
223
+ }
224
+ testSuiteFinishCh.publish({ status, errorMessage })
225
+ if (environment.global.__coverage__) {
226
+ const coverageFiles = extractCoverageInformation(environment.global.__coverage__, environment.rootDir)
227
+ if (coverageFiles.length) {
228
+ testSuiteCodeCoverageCh.publish([...coverageFiles, environment.testSuite])
229
+ }
230
+ }
231
+ return suiteResults
232
+ })
233
+ })
234
+ })
235
+ if (jestAdapter.default) {
236
+ jestAdapter.default = newAdapter
237
+ } else {
238
+ jestAdapter = newAdapter
239
+ }
240
+
241
+ return jestAdapter
242
+ }
243
+
244
+ addHook({
245
+ name: 'jest-circus',
246
+ file: 'build/legacy-code-todo-rewrite/jestAdapter.js',
247
+ versions: ['>=24.8.0']
248
+ }, jestAdapterWrapper)
249
+
250
+ function configureTestEnvironment (readConfigsResult) {
251
+ const { configs } = readConfigsResult
252
+ sessionAsyncResource.runInAsyncScope(() => {
253
+ testSessionConfigurationCh.publish(configs.map(config => config.testEnvironmentOptions))
254
+ })
255
+ }
256
+
257
+ function jestConfigAsyncWrapper (jestConfig) {
258
+ shimmer.wrap(jestConfig, 'readConfigs', readConfigs => async function () {
259
+ const readConfigsResult = await readConfigs.apply(this, arguments)
260
+ configureTestEnvironment(readConfigsResult)
261
+ return readConfigsResult
262
+ })
263
+ return jestConfig
264
+ }
265
+
266
+ function jestConfigSyncWrapper (jestConfig) {
267
+ shimmer.wrap(jestConfig, 'readConfigs', readConfigs => function () {
268
+ const readConfigsResult = readConfigs.apply(this, arguments)
269
+ configureTestEnvironment(readConfigsResult)
270
+ return readConfigsResult
271
+ })
272
+ return jestConfig
273
+ }
274
+
275
+ // from 25.1.0 on, readConfigs becomes async
276
+ addHook({
277
+ name: 'jest-config',
278
+ versions: ['>=25.1.0']
279
+ }, jestConfigAsyncWrapper)
280
+
281
+ addHook({
282
+ name: 'jest-config',
283
+ versions: ['24.8.0 - 24.9.0']
284
+ }, jestConfigSyncWrapper)
285
+
286
+ function jasmineAsyncInstallWraper (jasmineAsyncInstallExport) {
171
287
  return function (globalConfig, globalInput) {
172
288
  globalInput._ddtrace = global._ddtrace
173
289
  shimmer.wrap(globalInput.jasmine.Spec.prototype, 'execute', execute => function (onComplete) {
@@ -194,4 +310,10 @@ addHook({
194
310
  })
195
311
  return jasmineAsyncInstallExport.default(globalConfig, globalInput)
196
312
  }
197
- })
313
+ }
314
+
315
+ addHook({
316
+ name: 'jest-jasmine2',
317
+ versions: ['>=24.8.0'],
318
+ file: 'build/jasmineAsyncInstall.js'
319
+ }, jasmineAsyncInstallWraper)
@@ -19,7 +19,31 @@ const patched = new WeakSet()
19
19
 
20
20
  const testToAr = new WeakMap()
21
21
  const originalFns = new WeakMap()
22
- const testSuiteToAr = new WeakMap()
22
+ const testFileToSuiteAr = new Map()
23
+
24
+ function getSuitesByTestFile (root) {
25
+ const suitesByTestFile = {}
26
+ function getSuites (suite) {
27
+ if (suite.file) {
28
+ if (suitesByTestFile[suite.file]) {
29
+ suitesByTestFile[suite.file].push(suite)
30
+ } else {
31
+ suitesByTestFile[suite.file] = [suite]
32
+ }
33
+ }
34
+ suite.suites.forEach(suite => {
35
+ getSuites(suite)
36
+ })
37
+ }
38
+ getSuites(root)
39
+
40
+ const numSuitesByTestFile = Object.keys(suitesByTestFile).reduce((acc, testFile) => {
41
+ acc[testFile] = suitesByTestFile[testFile].length
42
+ return acc
43
+ }, {})
44
+
45
+ return { suitesByTestFile, numSuitesByTestFile }
46
+ }
23
47
 
24
48
  function getTestStatus (test) {
25
49
  if (test.pending) {
@@ -56,6 +80,8 @@ function mochaHook (Runner) {
56
80
  return run.apply(this, arguments)
57
81
  }
58
82
 
83
+ const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
84
+
59
85
  const testRunAsyncResource = new AsyncResource('bound-anonymous-fn')
60
86
 
61
87
  this.once('end', testRunAsyncResource.bind(function () {
@@ -65,6 +91,7 @@ function mochaHook (Runner) {
65
91
  } else if (this.failures !== 0) {
66
92
  status = 'fail'
67
93
  }
94
+ testFileToSuiteAr.clear()
68
95
  testRunFinishCh.publish(status)
69
96
  }))
70
97
 
@@ -75,34 +102,45 @@ function mochaHook (Runner) {
75
102
  }))
76
103
 
77
104
  this.on('suite', function (suite) {
78
- if (suite.root) {
105
+ if (suite.root || !suite.tests.length) {
79
106
  return
80
107
  }
81
- const asyncResource = new AsyncResource('bound-anonymous-fn')
82
-
83
- testSuiteToAr.set(suite, asyncResource)
84
-
85
- asyncResource.runInAsyncScope(() => {
86
- testSuiteStartCh.publish(suite)
87
- })
108
+ let asyncResource = testFileToSuiteAr.get(suite.file)
109
+ if (!asyncResource) {
110
+ asyncResource = new AsyncResource('bound-anonymous-fn')
111
+ testFileToSuiteAr.set(suite.file, asyncResource)
112
+ asyncResource.runInAsyncScope(() => {
113
+ testSuiteStartCh.publish(suite)
114
+ })
115
+ }
88
116
  })
89
117
 
90
118
  this.on('suite end', function (suite) {
91
119
  if (suite.root) {
92
120
  return
93
121
  }
122
+ const suitesInTestFile = suitesByTestFile[suite.file]
123
+
124
+ const isLastSuite = --numSuitesByTestFile[suite.file] === 0
125
+ if (!isLastSuite) {
126
+ return
127
+ }
128
+
94
129
  let status = 'pass'
95
- if (suite.pending) {
130
+ if (suitesInTestFile.every(suite => suite.pending)) {
96
131
  status = 'skip'
97
132
  } else {
98
- suite.eachTest(test => {
99
- if (test.state === 'failed' || test.timedOut) {
100
- status = 'fail'
101
- }
133
+ // has to check every test in the test file
134
+ suitesInTestFile.forEach(suite => {
135
+ suite.eachTest(test => {
136
+ if (test.state === 'failed' || test.timedOut) {
137
+ status = 'fail'
138
+ }
139
+ })
102
140
  })
103
141
  }
104
142
 
105
- const asyncResource = testSuiteToAr.get(suite)
143
+ const asyncResource = testFileToSuiteAr.get(suite.file)
106
144
  asyncResource.runInAsyncScope(() => {
107
145
  // get suite status
108
146
  testSuiteFinishCh.publish(status)
@@ -125,7 +163,7 @@ function mochaHook (Runner) {
125
163
  const status = getTestStatus(test)
126
164
 
127
165
  // if there are afterEach to be run, we don't finish the test yet
128
- if (!test.parent._afterEach.length) {
166
+ if (asyncResource && !test.parent._afterEach.length) {
129
167
  asyncResource.runInAsyncScope(() => {
130
168
  testFinishCh.publish(status)
131
169
  })
@@ -148,34 +186,38 @@ function mochaHook (Runner) {
148
186
  })
149
187
 
150
188
  this.on('fail', (testOrHook, err) => {
189
+ const testFile = testOrHook.file
151
190
  let test = testOrHook
152
191
  const isHook = testOrHook.type === 'hook'
153
192
  if (isHook && testOrHook.ctx) {
154
193
  test = testOrHook.ctx.currentTest
155
194
  }
156
- let asyncResource
195
+ let testAsyncResource
157
196
  if (test) {
158
- asyncResource = getTestAsyncResource(test)
197
+ testAsyncResource = getTestAsyncResource(test)
159
198
  }
160
- if (asyncResource) {
161
- asyncResource.runInAsyncScope(() => {
199
+ if (testAsyncResource) {
200
+ testAsyncResource.runInAsyncScope(() => {
162
201
  if (isHook) {
163
- err.message = `${testOrHook.title}: ${err.message}`
202
+ err.message = `${testOrHook.fullTitle()}: ${err.message}`
164
203
  errorCh.publish(err)
165
204
  // if it's a hook and it has failed, 'test end' will not be called
166
205
  testFinishCh.publish('fail')
167
206
  } else {
168
207
  errorCh.publish(err)
169
208
  }
170
- // we propagate the error to the suite
171
- const testSuiteAsyncResource = testSuiteToAr.get(test.parent)
172
- if (testSuiteAsyncResource) {
173
- const testSuiteError = new Error(`Test "${test.fullTitle()}" failed with message "${err.message}"`)
174
- testSuiteError.stack = err.stack
175
- testSuiteAsyncResource.runInAsyncScope(() => {
176
- testSuiteErrorCh.publish(testSuiteError)
177
- })
178
- }
209
+ })
210
+ }
211
+ const testSuiteAsyncResource = testFileToSuiteAr.get(testFile)
212
+
213
+ if (testSuiteAsyncResource) {
214
+ // we propagate the error to the suite
215
+ const testSuiteError = new Error(
216
+ `"${testOrHook.parent.fullTitle()}" failed with message "${err.message}"`
217
+ )
218
+ testSuiteError.stack = err.stack
219
+ testSuiteAsyncResource.runInAsyncScope(() => {
220
+ testSuiteErrorCh.publish(testSuiteError)
179
221
  })
180
222
  }
181
223
  })
@@ -189,7 +231,11 @@ function mochaHook (Runner) {
189
231
  } else {
190
232
  // if there is no async resource, the test has been skipped through `test.skip``
191
233
  const skippedTestAsyncResource = new AsyncResource('bound-anonymous-fn')
192
- testToAr.set(test, skippedTestAsyncResource)
234
+ if (test.fn) {
235
+ testToAr.set(test.fn, skippedTestAsyncResource)
236
+ } else {
237
+ testToAr.set(test, skippedTestAsyncResource)
238
+ }
193
239
  skippedTestAsyncResource.runInAsyncScope(() => {
194
240
  skipCh.publish(test)
195
241
  })
@@ -65,21 +65,25 @@ function wrapFindPageComponents (findPageComponents) {
65
65
  const result = findPageComponents.apply(this, arguments)
66
66
 
67
67
  if (result) {
68
- pageLoadChannel.publish({ page: pathname })
68
+ pageLoadChannel.publish({ page: getPagePath(pathname) })
69
69
  }
70
70
 
71
71
  return result
72
72
  }
73
73
  }
74
74
 
75
+ function getPagePath (page) {
76
+ return typeof page === 'object' ? page.pathname : page
77
+ }
78
+
75
79
  function getPageFromPath (page, dynamicRoutes = []) {
76
80
  for (const dynamicRoute of dynamicRoutes) {
77
81
  if (dynamicRoute.page.startsWith('/api') && dynamicRoute.match(page)) {
78
- return dynamicRoute.page
82
+ return getPagePath(dynamicRoute.page)
79
83
  }
80
84
  }
81
85
 
82
- return page
86
+ return getPagePath(page)
83
87
  }
84
88
 
85
89
  function instrument (req, res, handler) {