dd-trace 6.0.0-pre-5359bfc → 6.0.0-pre-8ec0cfe

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 (31) hide show
  1. package/README.md +1 -32
  2. package/index.d.ts +21 -0
  3. package/package.json +5 -5
  4. package/packages/datadog-instrumentations/src/express.js +20 -0
  5. package/packages/datadog-instrumentations/src/grpc/client.js +56 -36
  6. package/packages/datadog-instrumentations/src/jest.js +110 -4
  7. package/packages/datadog-instrumentations/src/next.js +17 -3
  8. package/packages/datadog-plugin-cypress/src/plugin.js +5 -3
  9. package/packages/datadog-plugin-grpc/src/client.js +16 -2
  10. package/packages/datadog-plugin-http/src/client.js +1 -1
  11. package/packages/datadog-plugin-jest/src/index.js +20 -4
  12. package/packages/dd-trace/src/appsec/addresses.js +2 -0
  13. package/packages/dd-trace/src/appsec/api_security_sampler.js +16 -3
  14. package/packages/dd-trace/src/appsec/channels.js +2 -1
  15. package/packages/dd-trace/src/appsec/index.js +17 -2
  16. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +83 -0
  17. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +59 -25
  18. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +13 -2
  19. package/packages/dd-trace/src/config.js +11 -4
  20. package/packages/dd-trace/src/datastreams/writer.js +2 -5
  21. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +5 -3
  22. package/packages/dd-trace/src/format.js +25 -1
  23. package/packages/dd-trace/src/noop/span.js +1 -0
  24. package/packages/dd-trace/src/opentelemetry/span.js +9 -2
  25. package/packages/dd-trace/src/opentracing/span.js +38 -0
  26. package/packages/dd-trace/src/opentracing/span_context.js +12 -6
  27. package/packages/dd-trace/src/opentracing/tracer.js +2 -1
  28. package/packages/dd-trace/src/plugins/ci_plugin.js +17 -3
  29. package/packages/dd-trace/src/plugins/util/test.js +32 -6
  30. package/packages/dd-trace/src/profiling/config.js +22 -22
  31. package/packages/dd-trace/src/telemetry/index.js +3 -0
package/README.md CHANGED
@@ -194,38 +194,7 @@ Regardless of where you open the issue, someone at Datadog will try to help.
194
194
 
195
195
  ## Bundling
196
196
 
197
- Generally, `dd-trace` works by intercepting `require()` calls that a Node.js application makes when loading modules. This includes modules that are built-in to Node.js, like the `fs` module for accessing the filesystem, as well as modules installed from the npm registry, like the `pg` database module.
198
-
199
- Also generally, bundlers work by crawling all of the `require()` calls that an application makes to files on disk, replacing the `require()` calls with custom code, and then concatenating all of the resulting JavaScript into one "bundled" file. When a built-in module is loaded, like `require('fs')`, that call can then remain the same in the resulting bundle.
200
-
201
- Fundamentally APM tools like `dd-trace` stop working at this point. Perhaps they continue to intercept the calls for built-in modules but don't intercept calls to third party libraries. This means that by default when you bundle a `dd-trace` app with a bundler it is likely to capture information about disk access (via `fs`) and outbound HTTP requests (via `http`), but will otherwise omit calls to third party libraries (like extracting incoming request route information for the `express` framework or showing which query is run for the `mysql` database client).
202
-
203
- To get around this, one can treat all third party modules, or at least third party modules that the APM needs to instrument, as being "external" to the bundler. With this setting the instrumented modules remain on disk and continue to be loaded via `require()` while the non-instrumented modules are bundled. Sadly this results in a build with many extraneous files and starts to defeat the purpose of bundling.
204
-
205
- For these reasons it's necessary to have custom-built bundler plugins. Such plugins are able to instruct the bundler on how to behave, injecting intermediary code and otherwise intercepting the "translated" `require()` calls. The result is that many more packages are then included in the bundled JavaScript file. Some applications can have 100% of modules bundled, however native modules still need to remain external to the bundle.
206
-
207
- ### ESBuild Support
208
-
209
- This library provides experimental ESBuild support in the form of an ESBuild plugin. Require the `dd-trace/esbuild` module when building your bundle to enable the plugin.
210
-
211
- Here's an example of how one might use `dd-trace` with ESBuild:
212
-
213
- ```javascript
214
- const ddPlugin = require('dd-trace/esbuild')
215
- const esbuild = require('esbuild')
216
-
217
- esbuild.build({
218
- entryPoints: ['app.js'],
219
- bundle: true,
220
- outfile: 'out.js',
221
- plugins: [ddPlugin],
222
- platform: 'node', // allows built-in modules to be required
223
- target: ['node18']
224
- }).catch((err) => {
225
- console.error(err)
226
- process.exit(1)
227
- })
228
- ```
197
+ If you would like to trace your bundled application then please read this page on [bundling and dd-trace](https://docs.datadoghq.com/tracing/trace_collection/automatic_instrumentation/dd_libraries/nodejs/#bundling). It includes information on how to use our ESBuild plugin and includes caveats for other bundlers.
229
198
 
230
199
 
231
200
  ## Security Vulnerabilities
package/index.d.ts CHANGED
@@ -143,6 +143,11 @@ export declare interface TraceOptions extends Analyzable {
143
143
  * The type of request.
144
144
  */
145
145
  type?: string
146
+
147
+ /**
148
+ * An array of span links
149
+ */
150
+ links?: Array<{ context: SpanContext, attributes?: Object }>
146
151
  }
147
152
 
148
153
  /**
@@ -154,6 +159,14 @@ export declare interface TraceOptions extends Analyzable {
154
159
  */
155
160
  export declare interface Span extends opentracing.Span {
156
161
  context (): SpanContext;
162
+
163
+ /**
164
+ * Causally links another span to the current span
165
+ * @param {SpanContext} context The context of the span to link to.
166
+ * @param {Object} attributes An optional key value pair of arbitrary values.
167
+ * @returns {void}
168
+ */
169
+ addLink (context: SpanContext, attributes?: Object): void;
157
170
  }
158
171
 
159
172
  /**
@@ -1903,6 +1916,14 @@ export namespace opentelemetry {
1903
1916
  * use the current time.
1904
1917
  */
1905
1918
  recordException(exception: Exception, time?: TimeInput): void;
1919
+
1920
+ /**
1921
+ * Causally links another span to the current span
1922
+ * @param {otel.SpanContext} context The context of the span to link to.
1923
+ * @param {SpanAttributes} attributes An optional key value pair of arbitrary values.
1924
+ * @returns {void}
1925
+ */
1926
+ addLink (context: otel.SpanContext, attributes?: SpanAttributes): void;
1906
1927
  }
1907
1928
 
1908
1929
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "6.0.0-pre-5359bfc",
3
+ "version": "6.0.0-pre-8ec0cfe",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -70,7 +70,7 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@datadog/native-appsec": "7.0.0",
73
- "@datadog/native-iast-rewriter": "2.2.2",
73
+ "@datadog/native-iast-rewriter": "2.2.3",
74
74
  "@datadog/native-iast-taint-tracking": "1.6.4",
75
75
  "@datadog/native-metrics": "^2.0.0",
76
76
  "@datadog/pprof": "5.0.0",
@@ -78,7 +78,7 @@
78
78
  "@opentelemetry/api": "^1.0.0",
79
79
  "@opentelemetry/core": "^1.14.0",
80
80
  "crypto-randomuuid": "^1.0.0",
81
- "dc-polyfill": "^0.1.2",
81
+ "dc-polyfill": "^0.1.4",
82
82
  "ignore": "^5.2.4",
83
83
  "import-in-the-middle": "^1.7.3",
84
84
  "int64-buffer": "^0.1.9",
@@ -106,7 +106,7 @@
106
106
  "@types/node": ">=18",
107
107
  "autocannon": "^4.5.2",
108
108
  "aws-sdk": "^2.1446.0",
109
- "axios": "^0.21.2",
109
+ "axios": "^1.6.7",
110
110
  "benchmark": "^2.1.4",
111
111
  "body-parser": "^1.20.2",
112
112
  "chai": "^4.3.7",
@@ -130,7 +130,7 @@
130
130
  "jszip": "^3.5.0",
131
131
  "knex": "^2.4.2",
132
132
  "mkdirp": "^3.0.1",
133
- "mocha": "8",
133
+ "mocha": "^9",
134
134
  "multer": "^1.4.5-lts.1",
135
135
  "nock": "^11.3.3",
136
136
  "nyc": "^15.1.0",
@@ -19,11 +19,31 @@ function wrapHandle (handle) {
19
19
 
20
20
  const wrapRouterMethod = createWrapRouterMethod('express')
21
21
 
22
+ const responseJsonChannel = channel('datadog:express:response:json:start')
23
+
24
+ function wrapResponseJson (json) {
25
+ return function wrappedJson (obj) {
26
+ if (responseJsonChannel.hasSubscribers) {
27
+ // backward compat as express 4.x supports deprecated 3.x signature
28
+ if (arguments.length === 2 && typeof arguments[1] !== 'number') {
29
+ obj = arguments[1]
30
+ }
31
+
32
+ responseJsonChannel.publish({ req: this.req, body: obj })
33
+ }
34
+
35
+ return json.apply(this, arguments)
36
+ }
37
+ }
38
+
22
39
  addHook({ name: 'express', versions: ['>=4'] }, express => {
23
40
  shimmer.wrap(express.application, 'handle', wrapHandle)
24
41
  shimmer.wrap(express.Router, 'use', wrapRouterMethod)
25
42
  shimmer.wrap(express.Router, 'route', wrapRouterMethod)
26
43
 
44
+ shimmer.wrap(express.response, 'json', wrapResponseJson)
45
+ shimmer.wrap(express.response, 'jsonp', wrapResponseJson)
46
+
27
47
  return express
28
48
  })
29
49
 
@@ -15,54 +15,52 @@ const errorChannel = channel('apm:grpc:client:request:error')
15
15
  const finishChannel = channel('apm:grpc:client:request:finish')
16
16
  const emitChannel = channel('apm:grpc:client:request:emit')
17
17
 
18
- function createWrapMakeRequest (type) {
18
+ function createWrapMakeRequest (type, hasPeer = false) {
19
19
  return function wrapMakeRequest (makeRequest) {
20
20
  return function (path) {
21
21
  const args = ensureMetadata(this, arguments, 4)
22
22
 
23
- return callMethod(this, makeRequest, args, path, args[4], type)
23
+ return callMethod(this, makeRequest, args, path, args[4], type, hasPeer)
24
24
  }
25
25
  }
26
26
  }
27
27
 
28
- function createWrapLoadPackageDefinition () {
28
+ function createWrapLoadPackageDefinition (hasPeer = false) {
29
29
  return function wrapLoadPackageDefinition (loadPackageDefinition) {
30
30
  return function (packageDef) {
31
31
  const result = loadPackageDefinition.apply(this, arguments)
32
32
 
33
33
  if (!result) return result
34
34
 
35
- wrapPackageDefinition(result)
35
+ wrapPackageDefinition(result, hasPeer)
36
36
 
37
37
  return result
38
38
  }
39
39
  }
40
40
  }
41
41
 
42
- function createWrapMakeClientConstructor () {
42
+ function createWrapMakeClientConstructor (hasPeer = false) {
43
43
  return function wrapMakeClientConstructor (makeClientConstructor) {
44
44
  return function (methods) {
45
45
  const ServiceClient = makeClientConstructor.apply(this, arguments)
46
-
47
- wrapClientConstructor(ServiceClient, methods)
48
-
46
+ wrapClientConstructor(ServiceClient, methods, hasPeer)
49
47
  return ServiceClient
50
48
  }
51
49
  }
52
50
  }
53
51
 
54
- function wrapPackageDefinition (def) {
52
+ function wrapPackageDefinition (def, hasPeer = false) {
55
53
  for (const name in def) {
56
54
  if (def[name].format) continue
57
55
  if (def[name].service && def[name].prototype) {
58
- wrapClientConstructor(def[name], def[name].service)
56
+ wrapClientConstructor(def[name], def[name].service, hasPeer)
59
57
  } else {
60
- wrapPackageDefinition(def[name])
58
+ wrapPackageDefinition(def[name], hasPeer)
61
59
  }
62
60
  }
63
61
  }
64
62
 
65
- function wrapClientConstructor (ServiceClient, methods) {
63
+ function wrapClientConstructor (ServiceClient, methods, hasPeer = false) {
66
64
  const proto = ServiceClient.prototype
67
65
 
68
66
  if (typeof methods !== 'object' || 'format' in methods) return
@@ -76,24 +74,23 @@ function wrapClientConstructor (ServiceClient, methods) {
76
74
  const type = getType(methods[name])
77
75
 
78
76
  if (methods[name]) {
79
- proto[name] = wrapMethod(proto[name], path, type)
77
+ proto[name] = wrapMethod(proto[name], path, type, hasPeer)
80
78
  }
81
79
 
82
80
  if (originalName) {
83
- proto[originalName] = wrapMethod(proto[originalName], path, type)
81
+ proto[originalName] = wrapMethod(proto[originalName], path, type, hasPeer)
84
82
  }
85
83
  })
86
84
  }
87
85
 
88
- function wrapMethod (method, path, type) {
86
+ function wrapMethod (method, path, type, hasPeer) {
89
87
  if (typeof method !== 'function' || patched.has(method)) {
90
88
  return method
91
89
  }
92
90
 
93
91
  const wrapped = function () {
94
92
  const args = ensureMetadata(this, arguments, 1)
95
-
96
- return callMethod(this, method, args, path, args[1], type)
93
+ return callMethod(this, method, args, path, args[1], type, hasPeer)
97
94
  }
98
95
 
99
96
  Object.assign(wrapped, method)
@@ -117,7 +114,20 @@ function wrapCallback (ctx, callback = () => { }) {
117
114
  }
118
115
  }
119
116
 
120
- function createWrapEmit (ctx) {
117
+ function createWrapEmit (ctx, hasPeer = false) {
118
+ const onStatusWithPeer = function (ctx, arg1, thisArg) {
119
+ ctx.result = arg1
120
+ ctx.peer = thisArg.getPeer()
121
+ finishChannel.publish(ctx)
122
+ }
123
+
124
+ const onStatusWithoutPeer = function (ctx, arg1, thisArg) {
125
+ ctx.result = arg1
126
+ finishChannel.publish(ctx)
127
+ }
128
+
129
+ const onStatus = hasPeer ? onStatusWithPeer : onStatusWithoutPeer
130
+
121
131
  return function wrapEmit (emit) {
122
132
  return function (event, arg1) {
123
133
  switch (event) {
@@ -126,8 +136,7 @@ function createWrapEmit (ctx) {
126
136
  errorChannel.publish(ctx)
127
137
  break
128
138
  case 'status':
129
- ctx.result = arg1
130
- finishChannel.publish(ctx)
139
+ onStatus(ctx, arg1, this)
131
140
  break
132
141
  }
133
142
 
@@ -138,7 +147,7 @@ function createWrapEmit (ctx) {
138
147
  }
139
148
  }
140
149
 
141
- function callMethod (client, method, args, path, metadata, type) {
150
+ function callMethod (client, method, args, path, metadata, type, hasPeer = false) {
142
151
  if (!startChannel.hasSubscribers) return method.apply(client, args)
143
152
 
144
153
  const length = args.length
@@ -159,7 +168,7 @@ function callMethod (client, method, args, path, metadata, type) {
159
168
  const call = method.apply(client, args)
160
169
 
161
170
  if (call && typeof call.emit === 'function') {
162
- shimmer.wrap(call, 'emit', createWrapEmit(ctx))
171
+ shimmer.wrap(call, 'emit', createWrapEmit(ctx, hasPeer))
163
172
  }
164
173
 
165
174
  return call
@@ -223,34 +232,45 @@ function getGrpc (client) {
223
232
  } while ((proto = Object.getPrototypeOf(proto)))
224
233
  }
225
234
 
226
- function patch (grpc) {
227
- const proto = grpc.Client.prototype
235
+ function patch (hasPeer = false) {
236
+ return function patch (grpc) {
237
+ const proto = grpc.Client.prototype
228
238
 
229
- instances.set(proto, grpc)
239
+ instances.set(proto, grpc)
230
240
 
231
- shimmer.wrap(proto, 'makeBidiStreamRequest', createWrapMakeRequest(types.bidi))
232
- shimmer.wrap(proto, 'makeClientStreamRequest', createWrapMakeRequest(types.clientStream))
233
- shimmer.wrap(proto, 'makeServerStreamRequest', createWrapMakeRequest(types.serverStream))
234
- shimmer.wrap(proto, 'makeUnaryRequest', createWrapMakeRequest(types.unary))
241
+ shimmer.wrap(proto, 'makeBidiStreamRequest', createWrapMakeRequest(types.bidi, hasPeer))
242
+ shimmer.wrap(proto, 'makeClientStreamRequest', createWrapMakeRequest(types.clientStream, hasPeer))
243
+ shimmer.wrap(proto, 'makeServerStreamRequest', createWrapMakeRequest(types.serverStream, hasPeer))
244
+ shimmer.wrap(proto, 'makeUnaryRequest', createWrapMakeRequest(types.unary, hasPeer))
235
245
 
236
- return grpc
246
+ return grpc
247
+ }
237
248
  }
238
249
 
239
250
  if (nodeMajor <= 14) {
240
- addHook({ name: 'grpc', versions: ['>=1.24.3'] }, patch)
251
+ addHook({ name: 'grpc', versions: ['>=1.24.3'] }, patch(true))
241
252
 
242
253
  addHook({ name: 'grpc', versions: ['>=1.24.3'], file: 'src/client.js' }, client => {
243
- shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor())
254
+ shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor(true))
244
255
 
245
256
  return client
246
257
  })
247
258
  }
248
259
 
249
- addHook({ name: '@grpc/grpc-js', versions: ['>=1.0.3'] }, patch)
260
+ addHook({ name: '@grpc/grpc-js', versions: ['>=1.0.3 <1.1.4'] }, patch(false))
261
+
262
+ addHook({ name: '@grpc/grpc-js', versions: ['>=1.0.3 <1.1.4'], file: 'build/src/make-client.js' }, client => {
263
+ shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor(false))
264
+ shimmer.wrap(client, 'loadPackageDefinition', createWrapLoadPackageDefinition(false))
265
+
266
+ return client
267
+ })
268
+
269
+ addHook({ name: '@grpc/grpc-js', versions: ['>=1.1.4'] }, patch(true))
250
270
 
251
- addHook({ name: '@grpc/grpc-js', versions: ['>=1.0.3'], file: 'build/src/make-client.js' }, client => {
252
- shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor())
253
- shimmer.wrap(client, 'loadPackageDefinition', createWrapLoadPackageDefinition())
271
+ addHook({ name: '@grpc/grpc-js', versions: ['>=1.1.4'], file: 'build/src/make-client.js' }, client => {
272
+ shimmer.wrap(client, 'makeClientConstructor', createWrapMakeClientConstructor(true))
273
+ shimmer.wrap(client, 'loadPackageDefinition', createWrapLoadPackageDefinition(true))
254
274
 
255
275
  return client
256
276
  })
@@ -38,10 +38,12 @@ const testErrCh = channel('ci:jest:test:err')
38
38
 
39
39
  const skippableSuitesCh = channel('ci:jest:test-suite:skippable')
40
40
  const libraryConfigurationCh = channel('ci:jest:library-configuration')
41
+ const knownTestsCh = channel('ci:jest:known-tests')
41
42
 
42
43
  const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
43
44
 
44
45
  let skippableSuites = []
46
+ let knownTests = []
45
47
  let isCodeCoverageEnabled = false
46
48
  let isSuitesSkippingEnabled = false
47
49
  let isUserCodeCoverageEnabled = false
@@ -49,6 +51,11 @@ let isSuitesSkipped = false
49
51
  let numSkippedSuites = 0
50
52
  let hasUnskippableSuites = false
51
53
  let hasForcedToRunSuites = false
54
+ let isEarlyFlakeDetectionEnabled = false
55
+ let earlyFlakeDetectionNumRetries = 0
56
+
57
+ const EFD_STRING = "Retried by Datadog's Early Flake Detection"
58
+ const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + ' \\(#\\d+\\): ', 'g')
52
59
 
53
60
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
54
61
 
@@ -62,6 +69,7 @@ const specStatusToTestStatus = {
62
69
 
63
70
  const asyncResources = new WeakMap()
64
71
  const originalTestFns = new WeakMap()
72
+ const retriedTestsToNumAttempts = new Map()
65
73
 
66
74
  // based on https://github.com/facebook/jest/blob/main/packages/jest-circus/src/formatNodeAssertErrors.ts#L41
67
75
  function formatJestError (errors) {
@@ -90,6 +98,14 @@ function getTestEnvironmentOptions (config) {
90
98
  return {}
91
99
  }
92
100
 
101
+ function getEfdTestName (testName, numAttempt) {
102
+ return `${EFD_STRING} (#${numAttempt}): ${testName}`
103
+ }
104
+
105
+ function removeEfdTestName (testName) {
106
+ return testName.replace(EFD_TEST_NAME_REGEX, '')
107
+ }
108
+
93
109
  function getWrappedEnvironment (BaseEnvironment, jestVersion) {
94
110
  return class DatadogEnvironment extends BaseEnvironment {
95
111
  constructor (config, context) {
@@ -101,6 +117,38 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
101
117
  this.global._ddtrace = global._ddtrace
102
118
 
103
119
  this.testEnvironmentOptions = getTestEnvironmentOptions(config)
120
+
121
+ this.isEarlyFlakeDetectionEnabled = this.testEnvironmentOptions._ddIsEarlyFlakeDetectionEnabled
122
+
123
+ if (this.isEarlyFlakeDetectionEnabled) {
124
+ earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
125
+ try {
126
+ this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
127
+ } catch (e) {
128
+ // If there has been an error parsing the tests, we'll disable Early Flake Deteciton
129
+ this.isEarlyFlakeDetectionEnabled = false
130
+ }
131
+ }
132
+ }
133
+
134
+ // Function that receives a list of known tests for a test service and
135
+ // returns the ones that belong to the current suite
136
+ getKnownTestsForSuite (knownTests) {
137
+ let knownTestsForSuite = knownTests
138
+ // If jest runs in band, the known tests are not serialized, so they're an array.
139
+ if (!Array.isArray(knownTests)) {
140
+ knownTestsForSuite = JSON.parse(knownTestsForSuite)
141
+ }
142
+ return knownTestsForSuite
143
+ .filter(test => test.includes(this.testSuite))
144
+ .map(test => test.replace(`jest.${this.testSuite}.`, '').trim())
145
+ }
146
+
147
+ // Add the `add_test` event we don't have the test object yet, so
148
+ // we use its describe block to get the full name
149
+ getTestNameFromAddTestEvent (event, state) {
150
+ const describeSuffix = getJestTestName(state.currentDescribeBlock)
151
+ return removeEfdTestName(`${describeSuffix} ${event.testName}`).trim()
104
152
  }
105
153
 
106
154
  async handleTestEvent (event, state) {
@@ -124,23 +172,55 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
124
172
  }
125
173
  }
126
174
  if (event.name === 'test_start') {
175
+ let isNewTest = false
176
+ let numEfdRetry = null
127
177
  const testParameters = getTestParametersString(this.nameToParams, event.test.name)
128
178
  // Async resource for this test is created here
129
179
  // It is used later on by the test_done handler
130
180
  const asyncResource = new AsyncResource('bound-anonymous-fn')
131
181
  asyncResources.set(event.test, asyncResource)
182
+ const testName = getJestTestName(event.test)
183
+
184
+ if (this.isEarlyFlakeDetectionEnabled) {
185
+ const originalTestName = removeEfdTestName(testName)
186
+ isNewTest = retriedTestsToNumAttempts.has(originalTestName)
187
+ if (isNewTest) {
188
+ numEfdRetry = retriedTestsToNumAttempts.get(originalTestName)
189
+ retriedTestsToNumAttempts.set(originalTestName, numEfdRetry + 1)
190
+ }
191
+ }
192
+
132
193
  asyncResource.runInAsyncScope(() => {
133
194
  testStartCh.publish({
134
- name: getJestTestName(event.test),
195
+ name: removeEfdTestName(testName),
135
196
  suite: this.testSuite,
136
197
  runner: 'jest-circus',
137
198
  testParameters,
138
- frameworkVersion: jestVersion
199
+ frameworkVersion: jestVersion,
200
+ isNew: isNewTest,
201
+ isEfdRetry: numEfdRetry > 0
139
202
  })
140
203
  originalTestFns.set(event.test, event.test.fn)
141
204
  event.test.fn = asyncResource.bind(event.test.fn)
142
205
  })
143
206
  }
207
+ if (event.name === 'add_test') {
208
+ if (this.isEarlyFlakeDetectionEnabled) {
209
+ const testName = this.getTestNameFromAddTestEvent(event, state)
210
+ const isNew = !this.knownTestsForThisSuite?.includes(testName)
211
+ const isSkipped = event.mode === 'todo' || event.mode === 'skip'
212
+ if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(testName)) {
213
+ retriedTestsToNumAttempts.set(testName, 0)
214
+ for (let retryIndex = 0; retryIndex < earlyFlakeDetectionNumRetries; retryIndex++) {
215
+ if (this.global.test) {
216
+ this.global.test(getEfdTestName(event.testName, retryIndex), event.fn, event.timeout)
217
+ } else {
218
+ log.error('Early flake detection could not retry test because global.test is undefined')
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
144
224
  if (event.name === 'test_done') {
145
225
  const asyncResource = asyncResources.get(event.test)
146
226
  asyncResource.runInAsyncScope(() => {
@@ -206,7 +286,7 @@ addHook({
206
286
  }
207
287
  // TODO: could we get the rootDir from each test?
208
288
  const [test] = shardedTests
209
- const rootDir = test && test.context && test.context.config && test.context.config.rootDir
289
+ const rootDir = test?.context?.config?.rootDir
210
290
 
211
291
  const jestSuitesToRun = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
212
292
 
@@ -247,11 +327,32 @@ function cliWrapper (cli, jestVersion) {
247
327
  if (!err) {
248
328
  isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
249
329
  isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
330
+ isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
331
+ earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
250
332
  }
251
333
  } catch (err) {
252
334
  log.error(err)
253
335
  }
254
336
 
337
+ if (isEarlyFlakeDetectionEnabled) {
338
+ const knownTestsPromise = new Promise((resolve) => {
339
+ onDone = resolve
340
+ })
341
+
342
+ sessionAsyncResource.runInAsyncScope(() => {
343
+ knownTestsCh.publish({ onDone })
344
+ })
345
+
346
+ try {
347
+ const { err, knownTests: receivedKnownTests } = await knownTestsPromise
348
+ if (!err) {
349
+ knownTests = receivedKnownTests
350
+ }
351
+ } catch (err) {
352
+ log.error(err)
353
+ }
354
+ }
355
+
255
356
  if (isSuitesSkippingEnabled) {
256
357
  const skippableSuitesPromise = new Promise((resolve) => {
257
358
  onDone = resolve
@@ -322,7 +423,8 @@ function cliWrapper (cli, jestVersion) {
322
423
  numSkippedSuites,
323
424
  hasUnskippableSuites,
324
425
  hasForcedToRunSuites,
325
- error
426
+ error,
427
+ isEarlyFlakeDetectionEnabled
326
428
  })
327
429
  })
328
430
 
@@ -438,6 +540,7 @@ function configureTestEnvironment (readConfigsResult) {
438
540
  // because `jestAdapterWrapper` runs in a different process. We have to go through `testEnvironmentOptions`
439
541
  configs.forEach(config => {
440
542
  config.testEnvironmentOptions._ddTestCodeCoverageEnabled = isCodeCoverageEnabled
543
+ config.testEnvironmentOptions._ddKnownTests = knownTests
441
544
  })
442
545
 
443
546
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
@@ -498,6 +601,9 @@ addHook({
498
601
  _ddForcedToRun,
499
602
  _ddUnskippable,
500
603
  _ddItrCorrelationId,
604
+ _ddKnownTests,
605
+ _ddIsEarlyFlakeDetectionEnabled,
606
+ _ddEarlyFlakeDetectionNumRetries,
501
607
  ...restOfTestEnvironmentOptions
502
608
  } = testEnvironmentOptions
503
609
 
@@ -290,9 +290,23 @@ addHook({
290
290
  shimmer.massWrap(request.NextRequest.prototype, ['text', 'json'], function (originalMethod) {
291
291
  return async function wrappedJson () {
292
292
  const body = await originalMethod.apply(this, arguments)
293
- bodyParsedChannel.publish({
294
- body
295
- })
293
+
294
+ bodyParsedChannel.publish({ body })
295
+
296
+ return body
297
+ }
298
+ })
299
+
300
+ shimmer.wrap(request.NextRequest.prototype, 'formData', function (originalFormData) {
301
+ return async function wrappedFormData () {
302
+ const body = await originalFormData.apply(this, arguments)
303
+
304
+ let normalizedBody = body
305
+ if (typeof body.entries === 'function') {
306
+ normalizedBody = Object.fromEntries(body.entries())
307
+ }
308
+ bodyParsedChannel.publish({ body: normalizedBody })
309
+
296
310
  return body
297
311
  }
298
312
  })
@@ -45,7 +45,8 @@ const {
45
45
  GIT_REPOSITORY_URL,
46
46
  GIT_COMMIT_SHA,
47
47
  GIT_BRANCH,
48
- CI_PROVIDER_NAME
48
+ CI_PROVIDER_NAME,
49
+ CI_WORKSPACE_PATH
49
50
  } = require('../../dd-trace/src/plugins/util/tags')
50
51
  const {
51
52
  OS_VERSION,
@@ -186,7 +187,8 @@ module.exports = (on, config) => {
186
187
  [RUNTIME_NAME]: runtimeName,
187
188
  [RUNTIME_VERSION]: runtimeVersion,
188
189
  [GIT_BRANCH]: branch,
189
- [CI_PROVIDER_NAME]: ciProviderName
190
+ [CI_PROVIDER_NAME]: ciProviderName,
191
+ [CI_WORKSPACE_PATH]: repositoryRoot
190
192
  } = testEnvironmentMetadata
191
193
 
192
194
  const isUnsupportedCIProvider = !ciProviderName
@@ -205,7 +207,7 @@ module.exports = (on, config) => {
205
207
  testLevel: 'test'
206
208
  }
207
209
 
208
- const codeOwnersEntries = getCodeOwnersFileEntries()
210
+ const codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot)
209
211
 
210
212
  let activeSpan = null
211
213
  let testSessionSpan = null
@@ -41,7 +41,6 @@ class GrpcClientPlugin extends ClientPlugin {
41
41
  'grpc.status.code': 0
42
42
  }
43
43
  }, false)
44
-
45
44
  // needed as precursor for peer.service
46
45
  if (method.service && method.package) {
47
46
  span.setTag('rpc.service', method.package + '.' + method.service)
@@ -68,7 +67,7 @@ class GrpcClientPlugin extends ClientPlugin {
68
67
  this.addError(error, span)
69
68
  }
70
69
 
71
- finish ({ span, result }) {
70
+ finish ({ span, result, peer }) {
72
71
  if (!span) return
73
72
 
74
73
  const { code, metadata } = result || {}
@@ -80,6 +79,21 @@ class GrpcClientPlugin extends ClientPlugin {
80
79
  addMetadataTags(span, metadata, metadataFilter, 'response')
81
80
  }
82
81
 
82
+ if (peer) {
83
+ // The only scheme we want to support here is ipv[46]:port, although
84
+ // more are supported by the library
85
+ // https://github.com/grpc/grpc/blob/v1.60.0/doc/naming.md
86
+ const parts = peer.split(':')
87
+ if (parts[parts.length - 1].match(/^\d+/)) {
88
+ const port = parts[parts.length - 1]
89
+ const ip = parts.slice(0, -1).join(':')
90
+ span.setTag('network.destination.ip', ip)
91
+ span.setTag('network.destination.port', port)
92
+ } else {
93
+ span.setTag('network.destination.ip', peer)
94
+ }
95
+ }
96
+
83
97
  this.tagPeerService(span)
84
98
  span.finish()
85
99
  }
@@ -122,7 +122,7 @@ class HttpClientPlugin extends ClientPlugin {
122
122
  // conditions for no error:
123
123
  // 1. not using a custom agent instance with custom timeout specified
124
124
  // 2. no invocation of `req.setTimeout`
125
- if (!args.options.agent?.options.timeout && !customRequestTimeout) return
125
+ if (!args.options.agent?.options?.timeout && !customRequestTimeout) return
126
126
 
127
127
  span.setTag('error', 1)
128
128
  }