dd-trace 4.38.1 → 4.40.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 (73) hide show
  1. package/LICENSE-3rdparty.csv +0 -3
  2. package/README.md +8 -18
  3. package/ci/init.js +7 -0
  4. package/ext/exporters.d.ts +1 -0
  5. package/ext/exporters.js +2 -1
  6. package/ext/tags.d.ts +1 -0
  7. package/ext/tags.js +1 -0
  8. package/index.d.ts +18 -3
  9. package/initialize.mjs +52 -0
  10. package/package.json +9 -12
  11. package/packages/datadog-instrumentations/src/amqplib.js +5 -2
  12. package/packages/datadog-instrumentations/src/apollo-server-core.js +0 -1
  13. package/packages/datadog-instrumentations/src/apollo-server.js +0 -1
  14. package/packages/datadog-instrumentations/src/body-parser.js +0 -1
  15. package/packages/datadog-instrumentations/src/check_require_cache.js +67 -5
  16. package/packages/datadog-instrumentations/src/cookie-parser.js +0 -1
  17. package/packages/datadog-instrumentations/src/express.js +0 -1
  18. package/packages/datadog-instrumentations/src/graphql.js +0 -2
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  20. package/packages/datadog-instrumentations/src/helpers/register.js +5 -2
  21. package/packages/datadog-instrumentations/src/http/server.js +0 -1
  22. package/packages/datadog-instrumentations/src/jest.js +6 -3
  23. package/packages/datadog-instrumentations/src/mocha/common.js +48 -0
  24. package/packages/datadog-instrumentations/src/mocha/main.js +487 -0
  25. package/packages/datadog-instrumentations/src/mocha/utils.js +306 -0
  26. package/packages/datadog-instrumentations/src/mocha/worker.js +51 -0
  27. package/packages/datadog-instrumentations/src/mocha.js +4 -673
  28. package/packages/datadog-instrumentations/src/openai.js +188 -17
  29. package/packages/datadog-instrumentations/src/playwright.js +4 -3
  30. package/packages/datadog-instrumentations/src/router.js +1 -1
  31. package/packages/datadog-instrumentations/src/selenium.js +13 -6
  32. package/packages/datadog-plugin-graphql/src/resolve.js +4 -0
  33. package/packages/datadog-plugin-mocha/src/index.js +82 -8
  34. package/packages/datadog-plugin-next/src/index.js +1 -2
  35. package/packages/datadog-plugin-openai/src/index.js +219 -73
  36. package/packages/dd-trace/src/appsec/addresses.js +4 -2
  37. package/packages/dd-trace/src/appsec/blocking.js +19 -25
  38. package/packages/dd-trace/src/appsec/channels.js +2 -1
  39. package/packages/dd-trace/src/appsec/graphql.js +10 -3
  40. package/packages/dd-trace/src/appsec/index.js +11 -4
  41. package/packages/dd-trace/src/appsec/rasp.js +35 -0
  42. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  43. package/packages/dd-trace/src/appsec/remote_config/index.js +1 -0
  44. package/packages/dd-trace/src/appsec/rule_manager.js +15 -25
  45. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +2 -5
  46. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +3 -1
  47. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -1
  48. package/packages/dd-trace/src/config.js +97 -22
  49. package/packages/dd-trace/src/constants.js +2 -0
  50. package/packages/dd-trace/src/encode/0.4.js +47 -8
  51. package/packages/dd-trace/src/exporter.js +1 -0
  52. package/packages/dd-trace/src/flare/file.js +44 -0
  53. package/packages/dd-trace/src/flare/index.js +98 -0
  54. package/packages/dd-trace/src/log/channels.js +54 -29
  55. package/packages/dd-trace/src/log/writer.js +7 -49
  56. package/packages/dd-trace/src/opentelemetry/span.js +8 -0
  57. package/packages/dd-trace/src/opentracing/propagation/text_map.js +57 -12
  58. package/packages/dd-trace/src/plugins/index.js +1 -0
  59. package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
  60. package/packages/dd-trace/src/plugins/util/test.js +6 -0
  61. package/packages/dd-trace/src/priority_sampler.js +8 -4
  62. package/packages/dd-trace/src/profiler.js +2 -1
  63. package/packages/dd-trace/src/profiling/config.js +1 -0
  64. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  65. package/packages/dd-trace/src/profiling/{ssi-telemetry.js → ssi-heuristics.js} +64 -36
  66. package/packages/dd-trace/src/profiling/ssi-telemetry-mock-profiler.js +4 -9
  67. package/packages/dd-trace/src/proxy.js +49 -15
  68. package/packages/dd-trace/src/ritm.js +13 -1
  69. package/packages/dd-trace/src/sampling_rule.js +2 -1
  70. package/packages/dd-trace/src/startup-log.js +19 -15
  71. package/packages/dd-trace/src/telemetry/index.js +6 -2
  72. package/packages/dd-trace/src/tracer.js +3 -0
  73. package/packages/dd-trace/src/plugins/util/ip_blocklist.js +0 -51
@@ -0,0 +1,487 @@
1
+ 'use strict'
2
+
3
+ const { createCoverageMap } = require('istanbul-lib-coverage')
4
+ const { addHook, channel, AsyncResource } = require('../helpers/instrument')
5
+ const shimmer = require('../../../datadog-shimmer')
6
+ const { isMarkedAsUnskippable } = require('../../../datadog-plugin-jest/src/util')
7
+ const log = require('../../../dd-trace/src/log')
8
+ const {
9
+ getTestSuitePath,
10
+ MOCHA_WORKER_TRACE_PAYLOAD_CODE,
11
+ fromCoverageMapToCoverage,
12
+ getCoveredFilenamesFromCoverage,
13
+ mergeCoverage,
14
+ resetCoverage
15
+ } = require('../../../dd-trace/src/plugins/util/test')
16
+
17
+ const {
18
+ isNewTest,
19
+ retryTest,
20
+ getSuitesByTestFile,
21
+ runnableWrapper,
22
+ getOnTestHandler,
23
+ getOnTestEndHandler,
24
+ getOnHookEndHandler,
25
+ getOnFailHandler,
26
+ getOnPendingHandler,
27
+ testFileToSuiteAr
28
+ } = require('./utils')
29
+ require('./common')
30
+
31
+ const testSessionAsyncResource = new AsyncResource('bound-anonymous-fn')
32
+ const patched = new WeakSet()
33
+ const newTests = {}
34
+ let suitesToSkip = []
35
+ const unskippableSuites = []
36
+ let isSuitesSkipped = false
37
+ let skippedSuites = []
38
+ let isEarlyFlakeDetectionEnabled = false
39
+ let isSuitesSkippingEnabled = false
40
+ let earlyFlakeDetectionNumRetries = 0
41
+ let knownTests = []
42
+ let itrCorrelationId = ''
43
+ let isForcedToRun = false
44
+
45
+ // We'll preserve the original coverage here
46
+ const originalCoverageMap = createCoverageMap()
47
+
48
+ // test channels
49
+ const testStartCh = channel('ci:mocha:test:start')
50
+
51
+ // test suite channels
52
+ const testSuiteStartCh = channel('ci:mocha:test-suite:start')
53
+ const testSuiteFinishCh = channel('ci:mocha:test-suite:finish')
54
+ const testSuiteErrorCh = channel('ci:mocha:test-suite:error')
55
+ const testSuiteCodeCoverageCh = channel('ci:mocha:test-suite:code-coverage')
56
+
57
+ // session channels
58
+ const libraryConfigurationCh = channel('ci:mocha:library-configuration')
59
+ const knownTestsCh = channel('ci:mocha:known-tests')
60
+ const skippableSuitesCh = channel('ci:mocha:test-suite:skippable')
61
+ const workerReportTraceCh = channel('ci:mocha:worker-report:trace')
62
+ const testSessionStartCh = channel('ci:mocha:session:start')
63
+ const testSessionFinishCh = channel('ci:mocha:session:finish')
64
+ const itrSkippedSuitesCh = channel('ci:mocha:itr:skipped-suites')
65
+
66
+ function getFilteredSuites (originalSuites) {
67
+ return originalSuites.reduce((acc, suite) => {
68
+ const testPath = getTestSuitePath(suite.file, process.cwd())
69
+ const shouldSkip = suitesToSkip.includes(testPath)
70
+ const isUnskippable = unskippableSuites.includes(suite.file)
71
+ if (shouldSkip && !isUnskippable) {
72
+ acc.skippedSuites.add(testPath)
73
+ } else {
74
+ acc.suitesToRun.push(suite)
75
+ }
76
+ return acc
77
+ }, { suitesToRun: [], skippedSuites: new Set() })
78
+ }
79
+
80
+ function getOnStartHandler (isParallel, frameworkVersion) {
81
+ return testSessionAsyncResource.bind(function () {
82
+ const processArgv = process.argv.slice(2).join(' ')
83
+ const command = `mocha ${processArgv}`
84
+ testSessionStartCh.publish({ command, frameworkVersion })
85
+ if (!isParallel && skippedSuites.length) {
86
+ itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion })
87
+ }
88
+ })
89
+ }
90
+
91
+ function getOnEndHandler (isParallel) {
92
+ return testSessionAsyncResource.bind(function () {
93
+ let status = 'pass'
94
+ let error
95
+ if (this.stats) {
96
+ status = this.stats.failures === 0 ? 'pass' : 'fail'
97
+ if (this.stats.tests === 0) {
98
+ status = 'skip'
99
+ }
100
+ } else if (this.failures !== 0) {
101
+ status = 'fail'
102
+ }
103
+
104
+ if (!isParallel && isEarlyFlakeDetectionEnabled) {
105
+ /**
106
+ * If Early Flake Detection (EFD) is enabled the logic is as follows:
107
+ * - If all attempts for a test are failing, the test has failed and we will let the test process fail.
108
+ * - If just a single attempt passes, we will prevent the test process from failing.
109
+ * The rationale behind is the following: you may still be able to block your CI pipeline by gating
110
+ * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
111
+ */
112
+ for (const tests of Object.values(newTests)) {
113
+ const failingNewTests = tests.filter(test => test.isFailed())
114
+ const areAllNewTestsFailing = failingNewTests.length === tests.length
115
+ if (failingNewTests.length && !areAllNewTestsFailing) {
116
+ this.stats.failures -= failingNewTests.length
117
+ this.failures -= failingNewTests.length
118
+ }
119
+ }
120
+ }
121
+
122
+ if (status === 'fail') {
123
+ error = new Error(`Failed tests: ${this.failures}.`)
124
+ }
125
+
126
+ testFileToSuiteAr.clear()
127
+
128
+ let testCodeCoverageLinesTotal
129
+ if (global.__coverage__) {
130
+ try {
131
+ testCodeCoverageLinesTotal = originalCoverageMap.getCoverageSummary().lines.pct
132
+ } catch (e) {
133
+ // ignore errors
134
+ }
135
+ // restore the original coverage
136
+ global.__coverage__ = fromCoverageMapToCoverage(originalCoverageMap)
137
+ }
138
+
139
+ testSessionFinishCh.publish({
140
+ status,
141
+ isSuitesSkipped,
142
+ testCodeCoverageLinesTotal,
143
+ numSkippedSuites: skippedSuites.length,
144
+ hasForcedToRunSuites: isForcedToRun,
145
+ hasUnskippableSuites: !!unskippableSuites.length,
146
+ error,
147
+ isEarlyFlakeDetectionEnabled,
148
+ isParallel
149
+ })
150
+ })
151
+ }
152
+
153
+ // In this hook we delay the execution with options.delay to grab library configuration,
154
+ // skippable and known tests.
155
+ // It is called but skipped in parallel mode.
156
+ addHook({
157
+ name: 'mocha',
158
+ versions: ['>=5.2.0'],
159
+ file: 'lib/mocha.js'
160
+ }, (Mocha) => {
161
+ const mochaRunAsyncResource = new AsyncResource('bound-anonymous-fn')
162
+ shimmer.wrap(Mocha.prototype, 'run', run => function () {
163
+ // Workers do not need to request any data, just run the tests
164
+ if (!testStartCh.hasSubscribers || process.env.MOCHA_WORKER_ID || this.options.parallel) {
165
+ return run.apply(this, arguments)
166
+ }
167
+
168
+ // `options.delay` does not work in parallel mode, so ITR and EFD can't work.
169
+ // TODO: use `lib/cli/run-helpers.js#runMocha` to get the data in parallel mode.
170
+ this.options.delay = true
171
+
172
+ const runner = run.apply(this, arguments)
173
+
174
+ this.files.forEach(path => {
175
+ const isUnskippable = isMarkedAsUnskippable({ path })
176
+ if (isUnskippable) {
177
+ unskippableSuites.push(path)
178
+ }
179
+ })
180
+
181
+ const onReceivedSkippableSuites = ({ err, skippableSuites, itrCorrelationId: responseItrCorrelationId }) => {
182
+ if (err) {
183
+ suitesToSkip = []
184
+ } else {
185
+ suitesToSkip = skippableSuites
186
+ itrCorrelationId = responseItrCorrelationId
187
+ }
188
+ // We remove the suites that we skip through ITR
189
+ const filteredSuites = getFilteredSuites(runner.suite.suites)
190
+ const { suitesToRun } = filteredSuites
191
+
192
+ isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length
193
+
194
+ log.debug(
195
+ () => `${suitesToRun.length} out of ${runner.suite.suites.length} suites are going to run.`
196
+ )
197
+
198
+ runner.suite.suites = suitesToRun
199
+
200
+ skippedSuites = Array.from(filteredSuites.skippedSuites)
201
+
202
+ global.run()
203
+ }
204
+
205
+ const onReceivedKnownTests = ({ err, knownTests: receivedKnownTests }) => {
206
+ if (err) {
207
+ knownTests = []
208
+ isEarlyFlakeDetectionEnabled = false
209
+ } else {
210
+ knownTests = receivedKnownTests
211
+ }
212
+
213
+ if (isSuitesSkippingEnabled) {
214
+ skippableSuitesCh.publish({
215
+ onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
216
+ })
217
+ } else {
218
+ global.run()
219
+ }
220
+ }
221
+
222
+ const onReceivedConfiguration = ({ err, libraryConfig }) => {
223
+ if (err || !skippableSuitesCh.hasSubscribers || !knownTestsCh.hasSubscribers) {
224
+ return global.run()
225
+ }
226
+
227
+ isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
228
+ isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
229
+ earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
230
+
231
+ if (isEarlyFlakeDetectionEnabled) {
232
+ knownTestsCh.publish({
233
+ onDone: mochaRunAsyncResource.bind(onReceivedKnownTests)
234
+ })
235
+ } else if (isSuitesSkippingEnabled) {
236
+ skippableSuitesCh.publish({
237
+ onDone: mochaRunAsyncResource.bind(onReceivedSkippableSuites)
238
+ })
239
+ } else {
240
+ global.run()
241
+ }
242
+ }
243
+
244
+ mochaRunAsyncResource.runInAsyncScope(() => {
245
+ libraryConfigurationCh.publish({
246
+ onDone: mochaRunAsyncResource.bind(onReceivedConfiguration)
247
+ })
248
+ })
249
+
250
+ return runner
251
+ })
252
+ return Mocha
253
+ })
254
+
255
+ // Only used to set `mocha.options.delay` to true in serial mode. When the mocha CLI is used,
256
+ // setting options.delay in Mocha#run is not enough to delay the execution.
257
+ // TODO: modify this hook to grab the data in parallel mode, so that ITR and EFD can work.
258
+ addHook({
259
+ name: 'mocha',
260
+ versions: ['>=5.2.0'],
261
+ file: 'lib/cli/run-helpers.js'
262
+ }, (run) => {
263
+ shimmer.wrap(run, 'runMocha', runMocha => async function () {
264
+ if (!testStartCh.hasSubscribers) {
265
+ return runMocha.apply(this, arguments)
266
+ }
267
+
268
+ const mocha = arguments[0]
269
+ /**
270
+ * This attaches `run` to the global context, which we'll call after
271
+ * our configuration and skippable suites requests
272
+ */
273
+ if (!mocha.options.parallel) {
274
+ mocha.options.delay = true
275
+ }
276
+ return runMocha.apply(this, arguments)
277
+ })
278
+ return run
279
+ })
280
+
281
+ // Only used in serial mode (no --parallel flag is passed)
282
+ // This hook is used to generate session, module, suite and test events
283
+ addHook({
284
+ name: 'mocha',
285
+ versions: ['>=5.2.0'],
286
+ file: 'lib/runner.js'
287
+ }, function (Runner, frameworkVersion) {
288
+ if (patched.has(Runner)) return Runner
289
+
290
+ patched.add(Runner)
291
+
292
+ shimmer.wrap(Runner.prototype, 'runTests', runTests => function (suite, fn) {
293
+ if (isEarlyFlakeDetectionEnabled) {
294
+ // by the time we reach `this.on('test')`, it is too late. We need to add retries here
295
+ suite.tests.forEach(test => {
296
+ if (!test.isPending() && isNewTest(test, knownTests)) {
297
+ test._ddIsNew = true
298
+ retryTest(test, earlyFlakeDetectionNumRetries)
299
+ }
300
+ })
301
+ }
302
+ return runTests.apply(this, arguments)
303
+ })
304
+
305
+ shimmer.wrap(Runner.prototype, 'run', run => function () {
306
+ if (!testStartCh.hasSubscribers) {
307
+ return run.apply(this, arguments)
308
+ }
309
+
310
+ const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
311
+
312
+ this.once('start', getOnStartHandler(false, frameworkVersion))
313
+
314
+ this.once('end', getOnEndHandler(false))
315
+
316
+ this.on('test', getOnTestHandler(true, newTests))
317
+
318
+ this.on('test end', getOnTestEndHandler())
319
+
320
+ // If the hook passes, 'hook end' will be emitted. Otherwise, 'fail' will be emitted
321
+ this.on('hook end', getOnHookEndHandler())
322
+
323
+ this.on('fail', getOnFailHandler(true))
324
+
325
+ this.on('pending', getOnPendingHandler())
326
+
327
+ this.on('suite', function (suite) {
328
+ if (suite.root || !suite.tests.length) {
329
+ return
330
+ }
331
+ let asyncResource = testFileToSuiteAr.get(suite.file)
332
+ if (!asyncResource) {
333
+ asyncResource = new AsyncResource('bound-anonymous-fn')
334
+ testFileToSuiteAr.set(suite.file, asyncResource)
335
+ const isUnskippable = unskippableSuites.includes(suite.file)
336
+ isForcedToRun = isUnskippable && suitesToSkip.includes(getTestSuitePath(suite.file, process.cwd()))
337
+ asyncResource.runInAsyncScope(() => {
338
+ testSuiteStartCh.publish({
339
+ testSuiteAbsolutePath: suite.file,
340
+ isUnskippable,
341
+ isForcedToRun,
342
+ itrCorrelationId
343
+ })
344
+ })
345
+ }
346
+ })
347
+
348
+ this.on('suite end', function (suite) {
349
+ if (suite.root) {
350
+ return
351
+ }
352
+ const suitesInTestFile = suitesByTestFile[suite.file]
353
+
354
+ const isLastSuite = --numSuitesByTestFile[suite.file] === 0
355
+ if (!isLastSuite) {
356
+ return
357
+ }
358
+
359
+ let status = 'pass'
360
+ if (suitesInTestFile.every(suite => suite.pending)) {
361
+ status = 'skip'
362
+ } else {
363
+ // has to check every test in the test file
364
+ suitesInTestFile.forEach(suite => {
365
+ suite.eachTest(test => {
366
+ if (test.state === 'failed' || test.timedOut) {
367
+ status = 'fail'
368
+ }
369
+ })
370
+ })
371
+ }
372
+
373
+ if (global.__coverage__) {
374
+ const coverageFiles = getCoveredFilenamesFromCoverage(global.__coverage__)
375
+
376
+ testSuiteCodeCoverageCh.publish({
377
+ coverageFiles,
378
+ suiteFile: suite.file
379
+ })
380
+ // We need to reset coverage to get a code coverage per suite
381
+ // Before that, we preserve the original coverage
382
+ mergeCoverage(global.__coverage__, originalCoverageMap)
383
+ resetCoverage(global.__coverage__)
384
+ }
385
+
386
+ const asyncResource = testFileToSuiteAr.get(suite.file)
387
+ asyncResource.runInAsyncScope(() => {
388
+ testSuiteFinishCh.publish(status)
389
+ })
390
+ })
391
+
392
+ return run.apply(this, arguments)
393
+ })
394
+
395
+ return Runner
396
+ })
397
+
398
+ // Used both in serial and parallel mode, and by both the main process and the workers
399
+ // Used to set the correct async resource to the test.
400
+ addHook({
401
+ name: 'mocha',
402
+ versions: ['>=5.2.0'],
403
+ file: 'lib/runnable.js'
404
+ }, runnableWrapper)
405
+
406
+ // Only used in parallel mode (--parallel flag is passed)
407
+ // Used to generate suite events and receive test payloads from workers
408
+ addHook({
409
+ name: 'workerpool',
410
+ // mocha@8.0.0 added parallel support and uses workerpool for it
411
+ // The version they use is 6.0.0:
412
+ // https://github.com/mochajs/mocha/blob/612fa31228c695f16173ac675f40ccdf26b4cfb5/package.json#L75
413
+ versions: ['>=6.0.0'],
414
+ file: 'src/WorkerHandler.js'
415
+ }, (workerHandlerPackage) => {
416
+ shimmer.wrap(workerHandlerPackage.prototype, 'exec', exec => function (message, [testSuiteAbsolutePath]) {
417
+ if (!testStartCh.hasSubscribers) {
418
+ return exec.apply(this, arguments)
419
+ }
420
+
421
+ this.worker.on('message', function (message) {
422
+ if (Array.isArray(message)) {
423
+ const [messageCode, payload] = message
424
+ if (messageCode === MOCHA_WORKER_TRACE_PAYLOAD_CODE) {
425
+ testSessionAsyncResource.runInAsyncScope(() => {
426
+ workerReportTraceCh.publish(payload)
427
+ })
428
+ }
429
+ }
430
+ })
431
+
432
+ const testSuiteAsyncResource = new AsyncResource('bound-anonymous-fn')
433
+ testSuiteAsyncResource.runInAsyncScope(() => {
434
+ testSuiteStartCh.publish({
435
+ testSuiteAbsolutePath
436
+ })
437
+ })
438
+
439
+ try {
440
+ const promise = exec.apply(this, arguments)
441
+ promise.then(
442
+ (result) => {
443
+ const status = result.failureCount === 0 ? 'pass' : 'fail'
444
+ testSuiteAsyncResource.runInAsyncScope(() => {
445
+ testSuiteFinishCh.publish(status)
446
+ })
447
+ },
448
+ (err) => {
449
+ testSuiteAsyncResource.runInAsyncScope(() => {
450
+ testSuiteErrorCh.publish(err)
451
+ testSuiteFinishCh.publish('fail')
452
+ })
453
+ }
454
+ )
455
+ return promise
456
+ } catch (err) {
457
+ testSuiteAsyncResource.runInAsyncScope(() => {
458
+ testSuiteErrorCh.publish(err)
459
+ testSuiteFinishCh.publish('fail')
460
+ })
461
+ throw err
462
+ }
463
+ })
464
+
465
+ return workerHandlerPackage
466
+ })
467
+
468
+ // Only used in parallel mode (--parallel flag is passed)
469
+ // Used to start and finish test session and test module
470
+ addHook({
471
+ name: 'mocha',
472
+ versions: ['>=5.2.0'],
473
+ file: 'lib/nodejs/parallel-buffered-runner.js'
474
+ }, (ParallelBufferedRunner, frameworkVersion) => {
475
+ shimmer.wrap(ParallelBufferedRunner.prototype, 'run', run => function () {
476
+ if (!testStartCh.hasSubscribers) {
477
+ return run.apply(this, arguments)
478
+ }
479
+
480
+ this.once('start', getOnStartHandler(true, frameworkVersion))
481
+ this.once('end', getOnEndHandler(true))
482
+
483
+ return run.apply(this, arguments)
484
+ })
485
+
486
+ return ParallelBufferedRunner
487
+ })