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.
- package/README.md +1 -32
- package/index.d.ts +21 -0
- package/package.json +5 -5
- package/packages/datadog-instrumentations/src/express.js +20 -0
- package/packages/datadog-instrumentations/src/grpc/client.js +56 -36
- package/packages/datadog-instrumentations/src/jest.js +110 -4
- package/packages/datadog-instrumentations/src/next.js +17 -3
- package/packages/datadog-plugin-cypress/src/plugin.js +5 -3
- package/packages/datadog-plugin-grpc/src/client.js +16 -2
- package/packages/datadog-plugin-http/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +20 -4
- package/packages/dd-trace/src/appsec/addresses.js +2 -0
- package/packages/dd-trace/src/appsec/api_security_sampler.js +16 -3
- package/packages/dd-trace/src/appsec/channels.js +2 -1
- package/packages/dd-trace/src/appsec/index.js +17 -2
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +83 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +59 -25
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +13 -2
- package/packages/dd-trace/src/config.js +11 -4
- package/packages/dd-trace/src/datastreams/writer.js +2 -5
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +5 -3
- package/packages/dd-trace/src/format.js +25 -1
- package/packages/dd-trace/src/noop/span.js +1 -0
- package/packages/dd-trace/src/opentelemetry/span.js +9 -2
- package/packages/dd-trace/src/opentracing/span.js +38 -0
- package/packages/dd-trace/src/opentracing/span_context.js +12 -6
- package/packages/dd-trace/src/opentracing/tracer.js +2 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +17 -3
- package/packages/dd-trace/src/plugins/util/test.js +32 -6
- package/packages/dd-trace/src/profiling/config.js +22 -22
- 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
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
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": "^
|
|
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": "
|
|
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
|
|
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 (
|
|
227
|
-
|
|
235
|
+
function patch (hasPeer = false) {
|
|
236
|
+
return function patch (grpc) {
|
|
237
|
+
const proto = grpc.Client.prototype
|
|
228
238
|
|
|
229
|
-
|
|
239
|
+
instances.set(proto, grpc)
|
|
230
240
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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
|
-
|
|
294
|
-
|
|
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
|
|
125
|
+
if (!args.options.agent?.options?.timeout && !customRequestTimeout) return
|
|
126
126
|
|
|
127
127
|
span.setTag('error', 1)
|
|
128
128
|
}
|