dd-trace 4.11.1 → 4.13.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 (34) hide show
  1. package/README.md +4 -9
  2. package/index.d.ts +43 -0
  3. package/package.json +3 -2
  4. package/packages/datadog-esbuild/index.js +29 -9
  5. package/packages/datadog-instrumentations/src/cucumber.js +30 -11
  6. package/packages/datadog-instrumentations/src/jest.js +22 -11
  7. package/packages/datadog-instrumentations/src/mocha.js +30 -8
  8. package/packages/datadog-instrumentations/src/next.js +102 -16
  9. package/packages/datadog-instrumentations/src/openai.js +1 -1
  10. package/packages/datadog-instrumentations/src/pg.js +46 -0
  11. package/packages/datadog-plugin-cucumber/src/index.js +14 -2
  12. package/packages/datadog-plugin-cypress/src/plugin.js +17 -8
  13. package/packages/datadog-plugin-graphql/src/index.js +3 -3
  14. package/packages/datadog-plugin-jest/src/index.js +10 -2
  15. package/packages/datadog-plugin-jest/src/util.js +10 -4
  16. package/packages/datadog-plugin-mocha/src/index.js +14 -2
  17. package/packages/datadog-plugin-next/src/index.js +11 -3
  18. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +19 -4
  19. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +1 -1
  20. package/packages/dd-trace/src/appsec/iast/telemetry/index.js +14 -5
  21. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +120 -10
  22. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +0 -1
  23. package/packages/dd-trace/src/dogstatsd.js +65 -5
  24. package/packages/dd-trace/src/exporters/agent/writer.js +9 -9
  25. package/packages/dd-trace/src/exporters/common/request.js +13 -4
  26. package/packages/dd-trace/src/opentracing/span.js +13 -13
  27. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  28. package/packages/dd-trace/src/plugins/ci_plugin.js +22 -1
  29. package/packages/dd-trace/src/plugins/util/test.js +18 -1
  30. package/packages/dd-trace/src/profiling/config.js +3 -1
  31. package/packages/dd-trace/src/profiling/profilers/wall.js +23 -7
  32. package/packages/dd-trace/src/proxy.js +23 -2
  33. package/packages/dd-trace/src/ritm.js +10 -2
  34. /package/packages/dd-trace/src/{metrics.js → runtime_metrics.js} +0 -0
package/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  [![npm v4](https://img.shields.io/npm/v/dd-trace/latest?color=blue&label=dd-trace%40v4&logo=npm)](https://www.npmjs.com/package/dd-trace)
4
4
  [![npm v3](https://img.shields.io/npm/v/dd-trace/latest-node14?color=blue&label=dd-trace%40v3&logo=npm)](https://www.npmjs.com/package/dd-trace/v/latest-node12)
5
- [![npm v2](https://img.shields.io/npm/v/dd-trace/latest-node12?color=blue&label=dd-trace%40v2&logo=npm)](https://www.npmjs.com/package/dd-trace/v/latest-node12)
6
- [![npm dev](https://img.shields.io/npm/v/dd-trace/dev?color=orange&label=dd-trace%40dev&logo=npm)](https://www.npmjs.com/package/dd-trace/v/dev)
7
5
  [![codecov](https://codecov.io/gh/DataDog/dd-trace-js/branch/master/graph/badge.svg)](https://codecov.io/gh/DataDog/dd-trace-js)
8
6
 
9
7
  <img align="right" src="https://user-images.githubusercontent.com/551402/208212084-1d0c07e2-4135-4c61-b2da-8f2fddbc66ed.png" alt="Bits the dog JavaScript" width="200px"/>
@@ -28,12 +26,12 @@ Most of the documentation for `dd-trace` is available on these webpages:
28
26
  | Release Line | Latest Version | Node.js | Status |Initial Release | End of Life |
29
27
  | :---: | :---: | :---: | :---: | :---: | :---: |
30
28
  | [`v1`](https://github.com/DataDog/dd-trace-js/tree/v1.x) | ![npm v1](https://img.shields.io/npm/v/dd-trace/legacy-v1?color=white&label=%20&style=flat-square) | `>= v12` | **End of Life** | 2021-07-13 | 2022-02-25 |
31
- | [`v2`](https://github.com/DataDog/dd-trace-js/tree/v2.x) | ![npm v2](https://img.shields.io/npm/v/dd-trace/latest-node12?color=white&label=%20&style=flat-square) | `>= v12` | **Maintenance** | 2022-01-28 | 2023-08-15 |
29
+ | [`v2`](https://github.com/DataDog/dd-trace-js/tree/v2.x) | ![npm v2](https://img.shields.io/npm/v/dd-trace/latest-node12?color=white&label=%20&style=flat-square) | `>= v12` | **End of Life** | 2022-01-28 | 2023-08-15 |
32
30
  | [`v3`](https://github.com/DataDog/dd-trace-js/tree/v3.x) | ![npm v3](https://img.shields.io/npm/v/dd-trace/latest-node14?color=white&label=%20&style=flat-square) | `>= v14` | **Maintenance** | 2022-08-15 | 2024-05-15 |
33
31
  | [`v4`](https://github.com/DataDog/dd-trace-js/tree/v4.x) | ![npm v4](https://img.shields.io/npm/v/dd-trace/latest?color=white&label=%20&style=flat-square) | `>= v16` | **Current** | 2023-05-12 | Unknown |
34
32
 
35
- We currently maintain three release lines, namely `v2`, `v3` and `v4`.
36
- Features and bug fixes that are merged are released to the `v4` line and, if appropriate, also the `v2` and `v3` line.
33
+ We currently maintain two release lines, namely `v3` and `v4`.
34
+ Features and bug fixes that are merged are released to the `v4` line and, if appropriate, also the `v3` line.
37
35
 
38
36
  For any new projects it is recommended to use the `v4` release line:
39
37
 
@@ -42,15 +40,12 @@ $ npm install dd-trace
42
40
  $ yarn add dd-trace
43
41
  ```
44
42
 
45
- However, existing projects that already use the `v2` or `v3` release lines, or projects that need to support EOL versions of Node.js, may continue to use these release lines.
43
+ However, existing projects that already use the `v3` release line, or projects that need to support EOL versions of Node.js, may continue to use these release lines.
46
44
  This is done by specifying the version when installing the package.
47
- Note that we also publish to npm using a `latest-node12` and `latest-node14` tag that can also be used for install:
48
45
 
49
46
  ```sh
50
47
  $ npm install dd-trace@3
51
48
  $ yarn add dd-trace@3
52
- $ npm install dd-trace@latest-node14
53
- $ yarn add dd-trace@latest-node14
54
49
  ```
55
50
 
56
51
  Any backwards-breaking functionality that is introduced into the library will result in an increase of the major version of the library and therefore a new release line.
package/index.d.ts CHANGED
@@ -121,6 +121,8 @@ export declare interface Tracer extends opentracing.Tracer {
121
121
  appsec: Appsec;
122
122
 
123
123
  TracerProvider: opentelemetry.TracerProvider;
124
+
125
+ dogstatsd: DogStatsD;
124
126
  }
125
127
 
126
128
  export declare interface TraceOptions extends Analyzable {
@@ -642,6 +644,47 @@ export declare interface User {
642
644
  [key: string]: string | undefined
643
645
  }
644
646
 
647
+ export declare interface DogStatsD {
648
+ /**
649
+ * Increments a metric by the specified value, optionally specifying tags.
650
+ * @param {string} stat The dot-separated metric name.
651
+ * @param {number} value The amount to increment the stat by.
652
+ * @param {[tag:string]:string|number} tags Tags to pass along, such as `[ 'foo:bar' ]`. Values are combined with config.tags.
653
+ */
654
+ increment(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
655
+
656
+ /**
657
+ * Decrements a metric by the specified value, optionally specifying tags.
658
+ * @param {string} stat The dot-separated metric name.
659
+ * @param {number} value The amount to decrement the stat by.
660
+ * @param {[tag:string]:string|number} tags Tags to pass along, such as `[ 'foo:bar' ]`. Values are combined with config.tags.
661
+ */
662
+ decrement(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
663
+
664
+ /**
665
+ * Sets a distribution value, optionally specifying tags.
666
+ * @param {string} stat The dot-separated metric name.
667
+ * @param {number} value The amount to increment the stat by.
668
+ * @param {[tag:string]:string|number} tags Tags to pass along, such as `[ 'foo:bar' ]`. Values are combined with config.tags.
669
+ */
670
+ distribution(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
671
+
672
+ /**
673
+ * Sets a gauge value, optionally specifying tags.
674
+ * @param {string} stat The dot-separated metric name.
675
+ * @param {number} value The amount to increment the stat by.
676
+ * @param {[tag:string]:string|number} tags Tags to pass along, such as `[ 'foo:bar' ]`. Values are combined with config.tags.
677
+ */
678
+ gauge(stat: string, value?: number, tags?: { [tag: string]: string|number }): void
679
+
680
+ /**
681
+ * Forces any unsent metrics to be sent
682
+ *
683
+ * @beta This method is experimental and could be removed in future versions.
684
+ */
685
+ flush(): void
686
+ }
687
+
645
688
  export declare interface Appsec {
646
689
  /**
647
690
  * Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "4.11.1",
3
+ "version": "4.13.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -37,6 +37,7 @@
37
37
  "test:integration:cypress": "mocha --colors --timeout 30000 \"integration-tests/cypress/*.spec.js\"",
38
38
  "test:integration:playwright": "mocha --colors --timeout 30000 \"integration-tests/playwright/*.spec.js\"",
39
39
  "test:integration:serverless": "mocha --colors --timeout 30000 \"integration-tests/serverless/*.spec.js\"",
40
+ "test:integration:plugins": "mocha --colors --timeout 30000 \"packages/datadog-plugin-*/test/integration-test/*.spec.js\"",
40
41
  "test:shimmer": "mocha --colors 'packages/datadog-shimmer/test/**/*.spec.js'",
41
42
  "test:shimmer:ci": "nyc --no-clean --include 'packages/datadog-shimmer/src/**/*.js' -- npm run test:shimmer",
42
43
  "leak:core": "node ./scripts/install_plugin_modules && (cd packages/memwatch && yarn) && NODE_PATH=./packages/memwatch/node_modules node --no-warnings ./node_modules/.bin/tape 'packages/dd-trace/test/leak/**/*.js'",
@@ -70,7 +71,7 @@
70
71
  "@datadog/native-iast-rewriter": "2.0.1",
71
72
  "@datadog/native-iast-taint-tracking": "1.5.0",
72
73
  "@datadog/native-metrics": "^2.0.0",
73
- "@datadog/pprof": "3.1.0",
74
+ "@datadog/pprof": "3.2.0",
74
75
  "@datadog/sketches-js": "^2.1.0",
75
76
  "@opentelemetry/api": "^1.0.0",
76
77
  "@opentelemetry/core": "^1.14.0",
@@ -50,21 +50,41 @@ for (const pkg of INSTRUMENTED) {
50
50
  module.exports.name = 'datadog-esbuild'
51
51
 
52
52
  module.exports.setup = function (build) {
53
+ const externalModules = new Set(build.initialOptions.external || [])
53
54
  build.onResolve({ filter: /.*/ }, args => {
55
+ if (externalModules.has(args.path)) {
56
+ // Internal Node.js packages will still be instrumented via require()
57
+ if (DEBUG) console.log(`EXTERNAL: ${args.path}`)
58
+ return
59
+ }
60
+
61
+ // TODO: Should this also check for namespace === 'file'?
62
+ if (args.path.startsWith('.') && !args.importer.includes('node_modules/')) {
63
+ // This is local application code, not an instrumented package
64
+ if (DEBUG) console.log(`LOCAL: ${args.path}`)
65
+ return
66
+ }
67
+
68
+ // TODO: Should this also check for namespace === 'file'?
69
+ if (args.path.startsWith('@') && !args.importer.includes('node_modules/')) {
70
+ // This is the Next.js convention for loading local files
71
+ if (DEBUG) console.log(`@LOCAL: ${args.path}`)
72
+ return
73
+ }
74
+
54
75
  let fullPathToModule
55
76
  try {
56
77
  fullPathToModule = dotFriendlyResolve(args.path, args.resolveDir)
57
78
  } catch (err) {
58
- console.warn(`Unable to find "${args.path}". Is the package dead code?`)
79
+ console.warn(`MISSING: Unable to find "${args.path}". Is the package dead code?`)
59
80
  return
60
81
  }
61
82
  const extracted = extractPackageAndModulePath(fullPathToModule)
62
- const packageName = args.path
63
83
 
64
84
  const internal = builtins.has(args.path)
65
85
 
66
86
  if (args.namespace === 'file' && (
67
- modulesOfInterest.has(packageName) || modulesOfInterest.has(`${extracted.pkg}/${extracted.path}`))
87
+ modulesOfInterest.has(args.path) || modulesOfInterest.has(`${extracted.pkg}/${extracted.path}`))
68
88
  ) {
69
89
  // The file namespace is used when requiring files from disk in userland
70
90
 
@@ -74,7 +94,7 @@ module.exports.setup = function (build) {
74
94
  } catch (err) {
75
95
  if (err.code === 'MODULE_NOT_FOUND') {
76
96
  if (!internal) {
77
- console.warn(`Unable to find "${extracted.pkg}/package.json". Is the package dead code?`)
97
+ console.warn(`MISSING: Unable to find "${extracted.pkg}/package.json". Is the package dead code?`)
78
98
  }
79
99
  return
80
100
  } else {
@@ -84,7 +104,7 @@ module.exports.setup = function (build) {
84
104
 
85
105
  const packageJson = require(pathToPackageJson)
86
106
 
87
- if (DEBUG) console.log(`RESOLVE ${packageName}@${packageJson.version}`)
107
+ if (DEBUG) console.log(`RESOLVE: ${args.path}@${packageJson.version}`)
88
108
 
89
109
  // https://esbuild.github.io/plugins/#on-resolve-arguments
90
110
  return {
@@ -95,17 +115,17 @@ module.exports.setup = function (build) {
95
115
  pkg: extracted.pkg,
96
116
  path: extracted.path,
97
117
  full: fullPathToModule,
98
- raw: packageName,
118
+ raw: args.path,
99
119
  internal
100
120
  }
101
121
  }
102
122
  } else if (args.namespace === NAMESPACE) {
103
123
  // The datadog namespace is used when requiring files that are injected during the onLoad stage
104
124
 
105
- if (builtins.has(packageName)) return
125
+ if (builtins.has(args.path)) return
106
126
 
107
127
  return {
108
- path: require.resolve(packageName, { paths: [ args.resolveDir ] }),
128
+ path: require.resolve(args.path, { paths: [ args.resolveDir ] }),
109
129
  namespace: 'file'
110
130
  }
111
131
  }
@@ -114,7 +134,7 @@ module.exports.setup = function (build) {
114
134
  build.onLoad({ filter: /.*/, namespace: NAMESPACE }, args => {
115
135
  const data = args.pluginData
116
136
 
117
- if (DEBUG) console.log(`LOAD ${data.pkg}@${data.version}, pkg "${data.path}"`)
137
+ if (DEBUG) console.log(`LOAD: ${data.pkg}@${data.version}, pkg "${data.path}"`)
118
138
 
119
139
  const path = data.raw !== data.pkg
120
140
  ? `${data.pkg}/${data.path}`
@@ -21,6 +21,8 @@ const skippableSuitesCh = channel('ci:cucumber:test-suite:skippable')
21
21
  const sessionStartCh = channel('ci:cucumber:session:start')
22
22
  const sessionFinishCh = channel('ci:cucumber:session:finish')
23
23
 
24
+ const itrSkippedSuitesCh = channel('ci:cucumber:itr:skipped-suites')
25
+
24
26
  const {
25
27
  getCoveredFilenamesFromCoverage,
26
28
  resetCoverage,
@@ -37,7 +39,6 @@ const patched = new WeakSet()
37
39
 
38
40
  let pickleByFile = {}
39
41
  const pickleResultByFile = {}
40
- let isSuitesSkipped = false
41
42
 
42
43
  function getSuiteStatusFromTestStatuses (testStatuses) {
43
44
  if (testStatuses.some(status => status === 'fail')) {
@@ -216,11 +217,18 @@ addHook({
216
217
  file: 'lib/runtime/test_case_runner.js'
217
218
  }, testCaseHook)
218
219
 
219
- function getPicklesToRun (runtime, suitesToSkip) {
220
- return runtime.pickleIds.filter((pickleId) => {
220
+ function getFilteredPickles (runtime, suitesToSkip) {
221
+ return runtime.pickleIds.reduce((acc, pickleId) => {
221
222
  const test = runtime.eventDataCollector.getPickle(pickleId)
222
- return !suitesToSkip.includes(getTestSuitePath(test.uri, process.cwd()))
223
- }, {})
223
+ const testSuitePath = getTestSuitePath(test.uri, process.cwd())
224
+ const isSkipped = suitesToSkip.includes(testSuitePath)
225
+ if (isSkipped) {
226
+ acc.skippedSuites.add(testSuitePath)
227
+ } else {
228
+ acc.picklesToRun.push(pickleId)
229
+ }
230
+ return acc
231
+ }, { skippedSuites: new Set(), picklesToRun: [] })
224
232
  }
225
233
 
226
234
  function getPickleByFile (runtime) {
@@ -239,7 +247,7 @@ addHook({
239
247
  name: '@cucumber/cucumber',
240
248
  versions: ['>=7.0.0'],
241
249
  file: 'lib/runtime/index.js'
242
- }, (runtimePackage, cucumberVersion) => {
250
+ }, (runtimePackage, frameworkVersion) => {
243
251
  shimmer.wrap(runtimePackage.default.prototype, 'start', start => async function () {
244
252
  const asyncResource = new AsyncResource('bound-anonymous-fn')
245
253
  let onDone
@@ -263,11 +271,16 @@ addHook({
263
271
  })
264
272
 
265
273
  const { err, skippableSuites } = await skippableSuitesPromise
274
+ let skippedSuites = []
275
+ let isSuitesSkipped = false
266
276
 
267
277
  if (!err) {
268
- const newPickleIds = getPicklesToRun(this, skippableSuites)
269
- isSuitesSkipped = newPickleIds.length !== this.pickleIds.length
270
- this.pickleIds = newPickleIds
278
+ const filteredPickles = getFilteredPickles(this, skippableSuites)
279
+ const { picklesToRun } = filteredPickles
280
+ isSuitesSkipped = picklesToRun.length !== this.pickleIds.length
281
+ this.pickleIds = picklesToRun
282
+
283
+ skippedSuites = Array.from(filteredPickles.skippedSuites)
271
284
  }
272
285
 
273
286
  pickleByFile = getPickleByFile(this)
@@ -276,8 +289,13 @@ addHook({
276
289
  const command = process.env.npm_lifecycle_script || `cucumber-js ${processArgv}`
277
290
 
278
291
  asyncResource.runInAsyncScope(() => {
279
- sessionStartCh.publish({ command, frameworkVersion: cucumberVersion })
292
+ sessionStartCh.publish({ command, frameworkVersion })
280
293
  })
294
+
295
+ if (!err && skippedSuites.length) {
296
+ itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion })
297
+ }
298
+
281
299
  const success = await start.apply(this, arguments)
282
300
 
283
301
  let testCodeCoverageLinesTotal
@@ -296,7 +314,8 @@ addHook({
296
314
  sessionFinishCh.publish({
297
315
  status: success ? 'pass' : 'fail',
298
316
  isSuitesSkipped,
299
- testCodeCoverageLinesTotal
317
+ testCodeCoverageLinesTotal,
318
+ numSkippedSuites: skippedSuites.length
300
319
  })
301
320
  })
302
321
  return success
@@ -39,10 +39,13 @@ const testErrCh = channel('ci:jest:test:err')
39
39
  const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
40
40
  const jestItrConfigurationCh = channel('ci:jest:itr-configuration')
41
41
 
42
+ const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
43
+
42
44
  let skippableSuites = []
43
45
  let isCodeCoverageEnabled = false
44
46
  let isSuitesSkippingEnabled = false
45
47
  let isSuitesSkipped = false
48
+ let numSkippedSuites = 0
46
49
 
47
50
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
48
51
 
@@ -102,7 +105,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
102
105
  await super.handleTestEvent(event, state)
103
106
  }
104
107
 
105
- const setNameToParams = (name, params) => { this.nameToParams[name] = params }
108
+ const setNameToParams = (name, params) => { this.nameToParams[name] = [...params] }
106
109
 
107
110
  if (event.name === 'setup') {
108
111
  if (this.global.test) {
@@ -191,7 +194,7 @@ addHook({
191
194
  addHook({
192
195
  name: '@jest/test-sequencer',
193
196
  versions: ['>=24.8.0']
194
- }, sequencerPackage => {
197
+ }, (sequencerPackage, frameworkVersion) => {
195
198
  shimmer.wrap(sequencerPackage.default.prototype, 'shard', shard => function () {
196
199
  const shardedTests = shard.apply(this, arguments)
197
200
 
@@ -202,13 +205,15 @@ addHook({
202
205
  const [test] = shardedTests
203
206
  const rootDir = test && test.context && test.context.config && test.context.config.rootDir
204
207
 
205
- const filteredTests = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
208
+ const { skippedSuites, suitesToRun } = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
206
209
 
207
- isSuitesSkipped = filteredTests.length !== shardedTests.length
210
+ isSuitesSkipped = suitesToRun.length !== shardedTests.length
211
+ numSkippedSuites = skippedSuites.length
208
212
 
209
- skippableSuites = []
213
+ itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion })
210
214
 
211
- return filteredTests
215
+ skippableSuites = []
216
+ return suitesToRun
212
217
  })
213
218
  return sequencerPackage
214
219
  })
@@ -279,10 +284,13 @@ function cliWrapper (cli, jestVersion) {
279
284
  isSuitesSkipped,
280
285
  isSuitesSkippingEnabled,
281
286
  isCodeCoverageEnabled,
282
- testCodeCoverageLinesTotal
287
+ testCodeCoverageLinesTotal,
288
+ numSkippedSuites
283
289
  })
284
290
  })
285
291
 
292
+ numSkippedSuites = 0
293
+
286
294
  return result
287
295
  })
288
296
 
@@ -468,7 +476,7 @@ addHook({
468
476
  name: '@jest/core',
469
477
  versions: ['>=24.8.0'],
470
478
  file: 'build/SearchSource.js'
471
- }, searchSourcePackage => {
479
+ }, (searchSourcePackage, frameworkVersion) => {
472
480
  const SearchSource = searchSourcePackage.default ? searchSourcePackage.default : searchSourcePackage
473
481
 
474
482
  shimmer.wrap(SearchSource.prototype, 'getTestPaths', getTestPaths => async function () {
@@ -492,13 +500,16 @@ addHook({
492
500
  const testPaths = await getTestPaths.apply(this, arguments)
493
501
  const { tests } = testPaths
494
502
 
495
- const filteredTests = getJestSuitesToRun(skippableSuites, tests, rootDir)
503
+ const { skippedSuites, suitesToRun } = getJestSuitesToRun(skippableSuites, tests, rootDir)
504
+
505
+ isSuitesSkipped = suitesToRun.length !== tests.length
506
+ numSkippedSuites = skippedSuites.length
496
507
 
497
- isSuitesSkipped = filteredTests.length !== tests.length
508
+ itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion })
498
509
 
499
510
  skippableSuites = []
500
511
 
501
- return { ...testPaths, tests: filteredTests }
512
+ return { ...testPaths, tests: suitesToRun }
502
513
  })
503
514
 
504
515
  return searchSourcePackage
@@ -30,6 +30,8 @@ const testSuiteFinishCh = channel('ci:mocha:test-suite:finish')
30
30
  const testSuiteErrorCh = channel('ci:mocha:test-suite:error')
31
31
  const testSuiteCodeCoverageCh = channel('ci:mocha:test-suite:code-coverage')
32
32
 
33
+ const itrSkippedSuitesCh = channel('ci:mocha:itr:skipped-suites')
34
+
33
35
  // TODO: remove when root hooks and fixtures are implemented
34
36
  const patched = new WeakSet()
35
37
 
@@ -47,6 +49,7 @@ const originalCoverageMap = createCoverageMap()
47
49
  let suitesToSkip = []
48
50
  let frameworkVersion
49
51
  let isSuitesSkipped = false
52
+ let skippedSuites = []
50
53
 
51
54
  function getSuitesByTestFile (root) {
52
55
  const suitesByTestFile = {}
@@ -97,10 +100,17 @@ function getTestAsyncResource (test) {
97
100
  return testToAr.get(originalFn)
98
101
  }
99
102
 
100
- function getSuitesToRun (originalSuites) {
101
- return originalSuites.filter(suite =>
102
- !suitesToSkip.includes(getTestSuitePath(suite.file, process.cwd()))
103
- )
103
+ function getFilteredSuites (originalSuites) {
104
+ return originalSuites.reduce((acc, suite) => {
105
+ const testPath = getTestSuitePath(suite.file, process.cwd())
106
+ const shouldSkip = suitesToSkip.includes(testPath)
107
+ if (shouldSkip) {
108
+ acc.skippedSuites.add(testPath)
109
+ } else {
110
+ acc.suitesToRun.push(suite)
111
+ }
112
+ return acc
113
+ }, { suitesToRun: [], skippedSuites: new Set() })
104
114
  }
105
115
 
106
116
  function mochaHook (Runner) {
@@ -137,13 +147,21 @@ function mochaHook (Runner) {
137
147
  global.__coverage__ = fromCoverageMapToCoverage(originalCoverageMap)
138
148
  }
139
149
 
140
- testSessionFinishCh.publish({ status, isSuitesSkipped, testCodeCoverageLinesTotal })
150
+ testSessionFinishCh.publish({
151
+ status,
152
+ isSuitesSkipped,
153
+ testCodeCoverageLinesTotal,
154
+ numSkippedSuites: skippedSuites.length
155
+ })
141
156
  }))
142
157
 
143
158
  this.once('start', testRunAsyncResource.bind(function () {
144
159
  const processArgv = process.argv.slice(2).join(' ')
145
160
  const command = `mocha ${processArgv}`
146
161
  testSessionStartCh.publish({ command, frameworkVersion })
162
+ if (skippedSuites.length) {
163
+ itrSkippedSuitesCh.publish({ skippedSuites, frameworkVersion })
164
+ }
147
165
  }))
148
166
 
149
167
  this.on('suite', function (suite) {
@@ -359,9 +377,13 @@ addHook({
359
377
  suitesToSkip = skippableSuites
360
378
  }
361
379
  // We remove the suites that we skip through ITR
362
- const newSuites = getSuitesToRun(runner.suite.suites)
363
- isSuitesSkipped = newSuites.length !== runner.suite.suites.length
364
- runner.suite.suites = newSuites
380
+ const filteredSuites = getFilteredSuites(runner.suite.suites)
381
+ const { suitesToRun } = filteredSuites
382
+
383
+ isSuitesSkipped = suitesToRun.length !== runner.suite.suites.length
384
+ runner.suite.suites = suitesToRun
385
+
386
+ skippedSuites = Array.from(filteredSuites.skippedSuites)
365
387
 
366
388
  global.run()
367
389
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  // TODO: either instrument all or none of the render functions
4
4
 
5
- const { channel, addHook, AsyncResource } = require('./helpers/instrument')
5
+ const { channel, addHook } = require('./helpers/instrument')
6
6
  const shimmer = require('../../datadog-shimmer')
7
7
  const { DD_MAJOR } = require('../../../version')
8
8
 
@@ -11,7 +11,8 @@ const finishChannel = channel('apm:next:request:finish')
11
11
  const errorChannel = channel('apm:next:request:error')
12
12
  const pageLoadChannel = channel('apm:next:page:load')
13
13
 
14
- const requestResources = new WeakMap()
14
+ const requests = new WeakSet()
15
+ const requestToNextjsPagePath = new WeakMap()
15
16
 
16
17
  function wrapHandleRequest (handleRequest) {
17
18
  return function (req, res, pathname, query) {
@@ -105,38 +106,91 @@ function getPageFromPath (page, dynamicRoutes = []) {
105
106
  }
106
107
 
107
108
  function instrument (req, res, handler) {
108
- if (requestResources.has(req)) return handler()
109
+ req = req.originalRequest || req
110
+ res = res.originalResponse || res
109
111
 
110
- const requestResource = new AsyncResource('bound-anonymous-fn')
112
+ if (requests.has(req)) return handler()
111
113
 
112
- requestResources.set(req, requestResource)
114
+ requests.add(req)
113
115
 
114
- return requestResource.runInAsyncScope(() => {
115
- startChannel.publish({ req, res })
116
+ const ctx = { req, res }
116
117
 
118
+ return startChannel.runStores(ctx, () => {
117
119
  try {
118
- const promise = handler()
120
+ const promise = handler(ctx)
119
121
 
120
122
  // promise should only reject when propagateError is true:
121
123
  // https://github.com/vercel/next.js/blob/cee656238a/packages/next/server/api-utils/node.ts#L547
122
124
  return promise.then(
123
- result => finish(req, res, result),
124
- err => finish(req, res, null, err)
125
+ result => finish(ctx, result),
126
+ err => finish(ctx, null, err)
125
127
  )
126
128
  } catch (e) {
127
129
  // this will probably never happen as the handler caller is an async function:
128
130
  // https://github.com/vercel/next.js/blob/cee656238a/packages/next/server/api-utils/node.ts#L420
129
- return finish(req, res, null, e)
131
+ return finish(ctx, null, e)
130
132
  }
131
133
  })
132
134
  }
133
135
 
134
- function finish (req, res, result, err) {
136
+ function wrapSetupServerWorker (setupServerWorker) {
137
+ return function (requestHandler) {
138
+ arguments[0] = shimmer.wrap(requestHandler, wrapRequestHandler(requestHandler))
139
+ return setupServerWorker.apply(this, arguments)
140
+ }
141
+ }
142
+
143
+ function wrapInitialize (initialize) {
144
+ return async function () {
145
+ const result = await initialize.apply(this, arguments)
146
+ if (Array.isArray(result)) {
147
+ const requestHandler = result[0]
148
+ result[0] = shimmer.wrap(requestHandler, wrapRequestHandler(requestHandler))
149
+ }
150
+ return result
151
+ }
152
+ }
153
+
154
+ function wrapRequestHandler (requestHandler) {
155
+ return function (req, res) {
156
+ return instrument(req, res, async () => {
157
+ const result = await requestHandler.apply(this, arguments) // apply here first to get page path association
158
+
159
+ const page = requestToNextjsPagePath.get(req)
160
+ if (page && pageLoadChannel.hasSubscribers) pageLoadChannel.publish({ page })
161
+
162
+ return result
163
+ })
164
+ }
165
+ }
166
+
167
+ // these two functions make sure we get path groups for routes in standalone,
168
+ // as it doesn't route through `next-server`/`base-server`
169
+ function wrapGetResolveRoutes (getResolveRoutes) {
170
+ return function () {
171
+ const result = getResolveRoutes.apply(this, arguments)
172
+ return shimmer.wrap(result, wrapResolveRoutes(result))
173
+ }
174
+ }
175
+
176
+ function wrapResolveRoutes (resolveRoutes) {
177
+ return async function (req) {
178
+ const result = await resolveRoutes.apply(this, arguments)
179
+ if (result && result.matchedOutput) {
180
+ const path = result.matchedOutput.itemPath
181
+ requestToNextjsPagePath.set(req, path)
182
+ }
183
+ return result
184
+ }
185
+ }
186
+
187
+ function finish (ctx, result, err) {
135
188
  if (err) {
136
- errorChannel.publish(err)
189
+ ctx.error = err
190
+ errorChannel.publish(ctx)
137
191
  }
138
192
 
139
- finishChannel.publish({ req, res })
193
+ finishChannel.publish(ctx)
140
194
 
141
195
  if (err) {
142
196
  throw err
@@ -145,11 +199,27 @@ function finish (req, res, result, err) {
145
199
  return result
146
200
  }
147
201
 
202
+ addHook({
203
+ name: 'next',
204
+ versions: ['>=13.4.13'],
205
+ file: 'dist/server/lib/router-utils/resolve-routes.js'
206
+ }, resolveRoutesModule => shimmer.wrap(resolveRoutesModule, 'getResolveRoutes', wrapGetResolveRoutes))
207
+
208
+ addHook({
209
+ name: 'next',
210
+ versions: ['13.4.13'],
211
+ file: 'dist/server/lib/setup-server-worker.js'
212
+ }, setupServerWorker => shimmer.wrap(setupServerWorker, 'initializeServerWorker', wrapSetupServerWorker))
213
+
214
+ addHook({
215
+ name: 'next',
216
+ versions: ['>=13.4.15'],
217
+ file: 'dist/server/lib/router-server.js'
218
+ }, routerServer => shimmer.wrap(routerServer, 'initialize', wrapInitialize))
219
+
148
220
  addHook({ name: 'next', versions: ['>=13.2'], file: 'dist/server/next-server.js' }, nextServer => {
149
221
  const Server = nextServer.default
150
222
 
151
- shimmer.wrap(Server.prototype, 'handleRequest', wrapHandleRequest)
152
- shimmer.wrap(Server.prototype, 'handleApiRequest', wrapHandleApiRequestWithMatch)
153
223
  shimmer.wrap(Server.prototype, 'renderToResponse', wrapRenderToResponse)
154
224
  shimmer.wrap(Server.prototype, 'renderErrorToResponse', wrapRenderErrorToResponse)
155
225
  shimmer.wrap(Server.prototype, 'findPageComponents', wrapFindPageComponents)
@@ -157,6 +227,22 @@ addHook({ name: 'next', versions: ['>=13.2'], file: 'dist/server/next-server.js'
157
227
  return nextServer
158
228
  })
159
229
 
230
+ // these functions wrapped in all versions above 13.2 except:
231
+ // 13.4.13 due to tests failing when these functions are wrapped
232
+ // 13.4.14 due to it not being in the NPM registry/officially released
233
+ addHook({
234
+ name: 'next',
235
+ versions: ['>=13.2 <13.4.13', '>=13.4.15'],
236
+ file: 'dist/server/next-server.js'
237
+ }, nextServer => {
238
+ const Server = nextServer.default
239
+
240
+ shimmer.wrap(Server.prototype, 'handleRequest', wrapHandleRequest)
241
+ shimmer.wrap(Server.prototype, 'handleApiRequest', wrapHandleApiRequestWithMatch)
242
+
243
+ return nextServer
244
+ })
245
+
160
246
  addHook({ name: 'next', versions: ['>=11.1 <13.2'], file: 'dist/server/next-server.js' }, nextServer => {
161
247
  const Server = nextServer.default
162
248
 
@@ -10,7 +10,7 @@ const startCh = channel('apm:openai:request:start')
10
10
  const finishCh = channel('apm:openai:request:finish')
11
11
  const errorCh = channel('apm:openai:request:error')
12
12
 
13
- addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0'] }, exports => {
13
+ addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, exports => {
14
14
  const methodNames = Object.getOwnPropertyNames(exports.OpenAIApi.prototype)
15
15
  methodNames.shift() // remove leading 'constructor' method
16
16