dd-trace 5.45.0 → 5.46.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.
- package/LICENSE-3rdparty.csv +1 -1
- package/ci/init.js +8 -0
- package/ext/exporters.d.ts +2 -1
- package/ext/exporters.js +2 -1
- package/package.json +3 -3
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +41 -1
- package/packages/datadog-instrumentations/src/mariadb.js +19 -0
- package/packages/datadog-instrumentations/src/playwright.js +321 -46
- package/packages/datadog-instrumentations/src/router.js +1 -7
- package/packages/datadog-plugin-mongodb-core/src/index.js +20 -0
- package/packages/datadog-plugin-playwright/src/index.js +115 -8
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +39 -15
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/command_injection.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
- package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/sql_injection.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +1 -1
- package/packages/dd-trace/src/appsec/rasp/utils.js +12 -7
- package/packages/dd-trace/src/appsec/recommended.json +256 -84
- package/packages/dd-trace/src/appsec/reporter.js +6 -4
- package/packages/dd-trace/src/appsec/telemetry/index.js +27 -3
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +70 -6
- package/packages/dd-trace/src/appsec/telemetry/waf.js +0 -30
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +8 -3
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +6 -4
- package/packages/dd-trace/src/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +102 -22
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +263 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +69 -36
- package/packages/dd-trace/src/debugger/devtools_client/lock.js +8 -0
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -7
- package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +15 -10
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +69 -62
- package/packages/dd-trace/src/debugger/devtools_client/state.js +3 -2
- package/packages/dd-trace/src/debugger/index.js +3 -0
- package/packages/dd-trace/src/encode/0.4.js +24 -17
- package/packages/dd-trace/src/exporter.js +1 -0
- package/packages/dd-trace/src/format.js +58 -60
- package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -3
- package/packages/dd-trace/src/opentelemetry/span.js +4 -4
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/config.js +6 -0
- package/packages/dd-trace/src/profiling/profiler.js +4 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +10 -8
- package/packages/dd-trace/src/tagger.js +38 -26
- package/packages/dd-trace/src/util.js +1 -7
package/LICENSE-3rdparty.csv
CHANGED
|
@@ -2,7 +2,7 @@ Component,Origin,License,Copyright
|
|
|
2
2
|
require,@datadog/libdatadog,Apache license 2.0,Copyright 2024 Datadog Inc.
|
|
3
3
|
require,@datadog/native-appsec,Apache license 2.0,Copyright 2018 Datadog Inc.
|
|
4
4
|
require,@datadog/native-metrics,Apache license 2.0,Copyright 2018 Datadog Inc.
|
|
5
|
-
require,@datadog/
|
|
5
|
+
require,@datadog/wasm-js-rewriter,Apache license 2.0,Copyright 2018 Datadog Inc.
|
|
6
6
|
require,@datadog/native-iast-taint-tracking,Apache license 2.0,Copyright 2018 Datadog Inc.
|
|
7
7
|
require,@datadog/pprof,Apache license 2.0,Copyright 2019 Google Inc.
|
|
8
8
|
require,@datadog/sketches-js,Apache license 2.0,Copyright 2020 Datadog Inc.
|
package/ci/init.js
CHANGED
|
@@ -7,6 +7,8 @@ const isJestWorker = !!process.env.JEST_WORKER_ID
|
|
|
7
7
|
const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID
|
|
8
8
|
const isMochaWorker = !!process.env.MOCHA_WORKER_ID
|
|
9
9
|
|
|
10
|
+
const isPlaywrightWorker = !!process.env.DD_PLAYWRIGHT_WORKER
|
|
11
|
+
|
|
10
12
|
const packageManagers = [
|
|
11
13
|
'npm',
|
|
12
14
|
'yarn',
|
|
@@ -67,6 +69,12 @@ if (isMochaWorker) {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
if (isPlaywrightWorker) {
|
|
73
|
+
options.experimental = {
|
|
74
|
+
exporter: 'playwright_worker'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
if (shouldInit) {
|
|
71
79
|
tracer.init(options)
|
|
72
80
|
tracer.use('fs', false)
|
package/ext/exporters.d.ts
CHANGED
package/ext/exporters.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.46.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -86,10 +86,10 @@
|
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"@datadog/libdatadog": "^0.5.0",
|
|
88
88
|
"@datadog/native-appsec": "8.5.1",
|
|
89
|
-
"@datadog/
|
|
89
|
+
"@datadog/wasm-js-rewriter": "3.1.0",
|
|
90
90
|
"@datadog/native-iast-taint-tracking": "3.3.0",
|
|
91
91
|
"@datadog/native-metrics": "^3.1.0",
|
|
92
|
-
"@datadog/pprof": "5.
|
|
92
|
+
"@datadog/pprof": "5.7.0",
|
|
93
93
|
"@datadog/sketches-js": "^2.1.0",
|
|
94
94
|
"@isaacs/ttlcache": "^1.4.1",
|
|
95
95
|
"@opentelemetry/api": ">=1.0.0 <1.9.0",
|
|
@@ -111,6 +111,7 @@ module.exports = {
|
|
|
111
111
|
pino: () => require('../pino'),
|
|
112
112
|
'pino-pretty': () => require('../pino'),
|
|
113
113
|
playwright: () => require('../playwright'),
|
|
114
|
+
'playwright-core': () => require('../playwright'),
|
|
114
115
|
'promise-js': () => require('../promise-js'),
|
|
115
116
|
promise: () => require('../promise'),
|
|
116
117
|
protobufjs: () => require('../protobufjs'),
|
|
@@ -51,6 +51,7 @@ if (DD_TRACE_DEBUG && DD_TRACE_DEBUG.toLowerCase() !== 'false') {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const seenCombo = new Set()
|
|
54
|
+
const allInstrumentations = {}
|
|
54
55
|
|
|
55
56
|
// TODO: make this more efficient
|
|
56
57
|
for (const packageName of names) {
|
|
@@ -67,6 +68,9 @@ for (const packageName of names) {
|
|
|
67
68
|
hook = hook.fn
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// get the instrumentation file name to save all hooked versions
|
|
72
|
+
const instrumentationFileName = parseHookInstrumentationFileName(packageName)
|
|
73
|
+
|
|
70
74
|
Hook([packageName], hookOptions, (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
|
|
71
75
|
moduleName = moduleName.replace(pathSepExpr, '/')
|
|
72
76
|
|
|
@@ -105,6 +109,7 @@ for (const packageName of names) {
|
|
|
105
109
|
let version = moduleVersion
|
|
106
110
|
try {
|
|
107
111
|
version = version || getVersion(moduleBaseDir)
|
|
112
|
+
allInstrumentations[instrumentationFileName] = allInstrumentations[instrumentationFileName] || false
|
|
108
113
|
} catch (e) {
|
|
109
114
|
log.error('Error getting version for "%s": %s', name, e.message, e)
|
|
110
115
|
continue
|
|
@@ -114,6 +119,8 @@ for (const packageName of names) {
|
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
if (matchVersion(version, versions)) {
|
|
122
|
+
allInstrumentations[instrumentationFileName] = true
|
|
123
|
+
|
|
117
124
|
// Check if the hook already has a set moduleExport
|
|
118
125
|
if (hook[HOOK_SYMBOL].has(moduleExports)) {
|
|
119
126
|
namesAndSuccesses[`${name}@${version}`] = true
|
|
@@ -143,7 +150,8 @@ for (const packageName of names) {
|
|
|
143
150
|
for (const nameVersion of Object.keys(namesAndSuccesses)) {
|
|
144
151
|
const [name, version] = nameVersion.split('@')
|
|
145
152
|
const success = namesAndSuccesses[nameVersion]
|
|
146
|
-
if
|
|
153
|
+
// we check allVersions to see if any version of the integration was successfully instrumented
|
|
154
|
+
if (!success && !seenCombo.has(nameVersion) && !allInstrumentations[instrumentationFileName]) {
|
|
147
155
|
telemetry('abort.integration', [
|
|
148
156
|
`integration:${name}`,
|
|
149
157
|
`integration_version:${version}`
|
|
@@ -171,6 +179,38 @@ function filename (name, file) {
|
|
|
171
179
|
return [name, file].filter(val => val).join('/')
|
|
172
180
|
}
|
|
173
181
|
|
|
182
|
+
// This function captures the instrumentation file name for a given package by parsing the hook require
|
|
183
|
+
// function given the module name. It is used to ensure that instrumentations such as redis
|
|
184
|
+
// that have several different modules being hooked, ie: 'redis' main package, and @redis/client submodule
|
|
185
|
+
// return a consistent instrumentation name. This is used later to ensure that atleast some portion of
|
|
186
|
+
// the integration was successfully instrumented. Prevents incorrect `Found incompatible integration version: ` messages
|
|
187
|
+
// Example:
|
|
188
|
+
// redis -> "() => require('../redis')" -> redis
|
|
189
|
+
// @redis/client -> "() => require('../redis')" -> redis
|
|
190
|
+
//
|
|
191
|
+
function parseHookInstrumentationFileName (packageName) {
|
|
192
|
+
let hook = hooks[packageName]
|
|
193
|
+
if (hook.fn) {
|
|
194
|
+
hook = hook.fn
|
|
195
|
+
}
|
|
196
|
+
const hookString = hook.toString()
|
|
197
|
+
|
|
198
|
+
const regex = /require\('([^']*)'\)/
|
|
199
|
+
const match = hookString.match(regex)
|
|
200
|
+
|
|
201
|
+
// try to capture the hook require file location.
|
|
202
|
+
if (match && match[1]) {
|
|
203
|
+
let moduleName = match[1]
|
|
204
|
+
// Remove leading '../' if present
|
|
205
|
+
if (moduleName.startsWith('../')) {
|
|
206
|
+
moduleName = moduleName.substring(3)
|
|
207
|
+
}
|
|
208
|
+
return moduleName
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
174
214
|
module.exports = {
|
|
175
215
|
filename,
|
|
176
216
|
pathSepExpr,
|
|
@@ -153,6 +153,18 @@ function wrapPoolMethod (createConnection) {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function wrapPoolGetConnectionMethod (getConnection) {
|
|
157
|
+
return function wrappedGetConnection () {
|
|
158
|
+
const cb = arguments[arguments.length - 1]
|
|
159
|
+
if (typeof cb !== 'function') return getConnection.apply(this, arguments)
|
|
160
|
+
|
|
161
|
+
const callbackResource = new AsyncResource('bound-anonymous-fn')
|
|
162
|
+
arguments[arguments.length - 1] = callbackResource.bind(cb)
|
|
163
|
+
|
|
164
|
+
return getConnection.apply(this, arguments)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
const name = 'mariadb'
|
|
157
169
|
|
|
158
170
|
addHook({ name, file: 'lib/cmd/query.js', versions: ['>=3'] }, (Query) => {
|
|
@@ -163,6 +175,13 @@ addHook({ name, file: 'lib/cmd/execute.js', versions: ['>=3'] }, (Execute) => {
|
|
|
163
175
|
return wrapCommand(Execute)
|
|
164
176
|
})
|
|
165
177
|
|
|
178
|
+
// in 3.4.1 getConnection method start to use callbacks instead of promises
|
|
179
|
+
addHook({ name, file: 'lib/pool.js', versions: ['>=3.4.1'] }, (Pool) => {
|
|
180
|
+
shimmer.wrap(Pool.prototype, 'getConnection', wrapPoolGetConnectionMethod)
|
|
181
|
+
|
|
182
|
+
return Pool
|
|
183
|
+
})
|
|
184
|
+
|
|
166
185
|
addHook({ name, file: 'lib/pool.js', versions: ['>=3'] }, (Pool) => {
|
|
167
186
|
shimmer.wrap(Pool.prototype, '_createConnection', wrapPoolMethod)
|
|
168
187
|
|
|
@@ -2,7 +2,11 @@ const satisfies = require('semifies')
|
|
|
2
2
|
|
|
3
3
|
const { addHook, channel, AsyncResource } = require('./helpers/instrument')
|
|
4
4
|
const shimmer = require('../../datadog-shimmer')
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
parseAnnotations,
|
|
7
|
+
getTestSuitePath,
|
|
8
|
+
PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE
|
|
9
|
+
} = require('../../dd-trace/src/plugins/util/test')
|
|
6
10
|
const log = require('../../dd-trace/src/log')
|
|
7
11
|
|
|
8
12
|
const testStartCh = channel('ci:playwright:test:start')
|
|
@@ -18,6 +22,9 @@ const testManagementTestsCh = channel('ci:playwright:test-management-tests')
|
|
|
18
22
|
const testSuiteStartCh = channel('ci:playwright:test-suite:start')
|
|
19
23
|
const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
|
|
20
24
|
|
|
25
|
+
const workerReportCh = channel('ci:playwright:worker:report')
|
|
26
|
+
const testPageGotoCh = channel('ci:playwright:test:page-goto')
|
|
27
|
+
|
|
21
28
|
const testToAr = new WeakMap()
|
|
22
29
|
const testSuiteToAr = new Map()
|
|
23
30
|
const testSuiteToTestStatuses = new Map()
|
|
@@ -255,21 +262,19 @@ function getTestFullname (test) {
|
|
|
255
262
|
return names.join(' ')
|
|
256
263
|
}
|
|
257
264
|
|
|
258
|
-
function testBeginHandler (test, browserName) {
|
|
265
|
+
function testBeginHandler (test, browserName, isMainProcess) {
|
|
259
266
|
const {
|
|
260
267
|
_requireFile: testSuiteAbsolutePath,
|
|
261
|
-
_type,
|
|
262
268
|
location: {
|
|
263
269
|
line: testSourceLine
|
|
264
|
-
}
|
|
270
|
+
},
|
|
271
|
+
_type
|
|
265
272
|
} = test
|
|
266
273
|
|
|
267
274
|
if (_type === 'beforeAll' || _type === 'afterAll') {
|
|
268
275
|
return
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
const testName = getTestFullname(test)
|
|
272
|
-
|
|
273
278
|
const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath)
|
|
274
279
|
|
|
275
280
|
if (isNewTestSuite) {
|
|
@@ -286,24 +291,31 @@ function testBeginHandler (test, browserName) {
|
|
|
286
291
|
test.retries = 0
|
|
287
292
|
}
|
|
288
293
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
// this handles tests that do not go through the worker process (because they're skipped)
|
|
295
|
+
if (isMainProcess) {
|
|
296
|
+
const testAsyncResource = new AsyncResource('bound-anonymous-fn')
|
|
297
|
+
testToAr.set(test, testAsyncResource)
|
|
298
|
+
const testName = getTestFullname(test)
|
|
299
|
+
|
|
300
|
+
testAsyncResource.runInAsyncScope(() => {
|
|
301
|
+
testStartCh.publish({
|
|
302
|
+
testName,
|
|
303
|
+
testSuiteAbsolutePath,
|
|
304
|
+
testSourceLine,
|
|
305
|
+
browserName,
|
|
306
|
+
isDisabled: test._ddIsDisabled
|
|
307
|
+
})
|
|
298
308
|
})
|
|
299
|
-
}
|
|
309
|
+
}
|
|
300
310
|
}
|
|
301
|
-
|
|
311
|
+
|
|
312
|
+
function testEndHandler (test, annotations, testStatus, error, isTimeout, isMainProcess) {
|
|
313
|
+
const { _requireFile: testSuiteAbsolutePath, results, _type } = test
|
|
314
|
+
|
|
302
315
|
let annotationTags
|
|
303
316
|
if (annotations.length) {
|
|
304
317
|
annotationTags = parseAnnotations(annotations)
|
|
305
318
|
}
|
|
306
|
-
const { _requireFile: testSuiteAbsolutePath, results, _type } = test
|
|
307
319
|
|
|
308
320
|
if (_type === 'beforeAll' || _type === 'afterAll') {
|
|
309
321
|
const hookError = formatTestHookError(error, _type, isTimeout)
|
|
@@ -324,35 +336,35 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout) {
|
|
|
324
336
|
testStatuses.push(testStatus)
|
|
325
337
|
}
|
|
326
338
|
|
|
327
|
-
let hasFailedAllRetries = false
|
|
328
|
-
let hasPassedAttemptToFixRetries = false
|
|
329
|
-
|
|
330
339
|
if (testStatuses.length === testManagementAttemptToFixRetries + 1) {
|
|
331
340
|
if (testStatuses.every(status => status === 'fail')) {
|
|
332
|
-
|
|
341
|
+
test._ddHasFailedAllRetries = true
|
|
333
342
|
} else if (testStatuses.every(status => status === 'pass')) {
|
|
334
|
-
|
|
343
|
+
test._ddHasPassedAttemptToFixRetries = true
|
|
335
344
|
}
|
|
336
345
|
}
|
|
337
346
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
347
|
+
// this handles tests that do not go through the worker process (because they're skipped)
|
|
348
|
+
if (isMainProcess) {
|
|
349
|
+
const testResult = results[results.length - 1]
|
|
350
|
+
const testAsyncResource = testToAr.get(test)
|
|
351
|
+
testAsyncResource.runInAsyncScope(() => {
|
|
352
|
+
testFinishCh.publish({
|
|
353
|
+
testStatus,
|
|
354
|
+
steps: testResult?.steps || [],
|
|
355
|
+
isRetry: testResult?.retry > 0,
|
|
356
|
+
error,
|
|
357
|
+
extraTags: annotationTags,
|
|
358
|
+
isNew: test._ddIsNew,
|
|
359
|
+
isAttemptToFix: test._ddIsAttemptToFix,
|
|
360
|
+
isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
|
|
361
|
+
isQuarantined: test._ddIsQuarantined,
|
|
362
|
+
isEfdRetry: test._ddIsEfdRetry,
|
|
363
|
+
hasFailedAllRetries: test._ddHasFailedAllRetries,
|
|
364
|
+
hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries
|
|
365
|
+
})
|
|
354
366
|
})
|
|
355
|
-
}
|
|
367
|
+
}
|
|
356
368
|
|
|
357
369
|
if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
|
|
358
370
|
testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus)
|
|
@@ -416,7 +428,7 @@ function dispatcherHook (dispatcherExport) {
|
|
|
416
428
|
const { test } = dispatcher._testById.get(params.testId)
|
|
417
429
|
const projects = getProjectsFromDispatcher(dispatcher)
|
|
418
430
|
const browser = getBrowserNameFromProjects(projects, test)
|
|
419
|
-
testBeginHandler(test, browser)
|
|
431
|
+
testBeginHandler(test, browser, true)
|
|
420
432
|
} else if (method === 'testEnd') {
|
|
421
433
|
const { test } = dispatcher._testById.get(params.testId)
|
|
422
434
|
|
|
@@ -424,7 +436,14 @@ function dispatcherHook (dispatcherExport) {
|
|
|
424
436
|
const testResult = results[results.length - 1]
|
|
425
437
|
|
|
426
438
|
const isTimeout = testResult.status === 'timedOut'
|
|
427
|
-
testEndHandler(
|
|
439
|
+
testEndHandler(
|
|
440
|
+
test,
|
|
441
|
+
params.annotations,
|
|
442
|
+
STATUS_TO_TEST_STATUS[testResult.status],
|
|
443
|
+
testResult.error,
|
|
444
|
+
isTimeout,
|
|
445
|
+
true
|
|
446
|
+
)
|
|
428
447
|
}
|
|
429
448
|
})
|
|
430
449
|
|
|
@@ -443,13 +462,28 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
|
|
|
443
462
|
const test = getTestByTestId(dispatcher, testId)
|
|
444
463
|
const projects = getProjectsFromDispatcher(dispatcher)
|
|
445
464
|
const browser = getBrowserNameFromProjects(projects, test)
|
|
446
|
-
testBeginHandler(test, browser)
|
|
465
|
+
testBeginHandler(test, browser, false)
|
|
447
466
|
})
|
|
448
467
|
worker.on('testEnd', ({ testId, status, errors, annotations }) => {
|
|
449
468
|
const test = getTestByTestId(dispatcher, testId)
|
|
450
469
|
|
|
451
470
|
const isTimeout = status === 'timedOut'
|
|
452
|
-
testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout)
|
|
471
|
+
testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout, false)
|
|
472
|
+
// We want to send the ddProperties to the worker
|
|
473
|
+
worker.process.send({
|
|
474
|
+
type: 'ddProperties',
|
|
475
|
+
testId: test.id,
|
|
476
|
+
properties: {
|
|
477
|
+
_ddIsDisabled: test._ddIsDisabled,
|
|
478
|
+
_ddIsQuarantined: test._ddIsQuarantined,
|
|
479
|
+
_ddIsAttemptToFix: test._ddIsAttemptToFix,
|
|
480
|
+
_ddIsAttemptToFixRetry: test._ddIsAttemptToFixRetry,
|
|
481
|
+
_ddIsNew: test._ddIsNew,
|
|
482
|
+
_ddIsEfdRetry: test._ddIsEfdRetry,
|
|
483
|
+
_ddHasFailedAllRetries: test._ddHasFailedAllRetries,
|
|
484
|
+
_ddHasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries
|
|
485
|
+
}
|
|
486
|
+
})
|
|
453
487
|
})
|
|
454
488
|
|
|
455
489
|
return worker
|
|
@@ -538,8 +572,8 @@ function runnerHook (runnerExport, playwrightVersion) {
|
|
|
538
572
|
// because they were skipped
|
|
539
573
|
tests.forEach(test => {
|
|
540
574
|
const browser = getBrowserNameFromProjects(projects, test)
|
|
541
|
-
testBeginHandler(test, browser)
|
|
542
|
-
testEndHandler(test, [], 'skip')
|
|
575
|
+
testBeginHandler(test, browser, true)
|
|
576
|
+
testEndHandler(test, [], 'skip', null, false, true)
|
|
543
577
|
})
|
|
544
578
|
})
|
|
545
579
|
|
|
@@ -720,3 +754,244 @@ addHook({
|
|
|
720
754
|
|
|
721
755
|
return loadUtilsPackage
|
|
722
756
|
})
|
|
757
|
+
|
|
758
|
+
// main process hook
|
|
759
|
+
addHook({
|
|
760
|
+
name: 'playwright',
|
|
761
|
+
file: 'lib/runner/processHost.js',
|
|
762
|
+
versions: ['>=1.38.0']
|
|
763
|
+
}, (processHostPackage) => {
|
|
764
|
+
shimmer.wrap(processHostPackage.ProcessHost.prototype, 'startRunner', startRunner => async function () {
|
|
765
|
+
this._extraEnv = {
|
|
766
|
+
...this._extraEnv,
|
|
767
|
+
// Used to detect that we're in a playwright worker
|
|
768
|
+
DD_PLAYWRIGHT_WORKER: '1'
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const res = await startRunner.apply(this, arguments)
|
|
772
|
+
|
|
773
|
+
// We add a new listener to `this.process`, which is represents the worker
|
|
774
|
+
this.process.on('message', (message) => {
|
|
775
|
+
// These messages are [code, payload]. The payload is test data
|
|
776
|
+
if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) {
|
|
777
|
+
workerReportCh.publish(message[1])
|
|
778
|
+
}
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
return res
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
return processHostPackage
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
addHook({
|
|
788
|
+
name: 'playwright-core',
|
|
789
|
+
file: 'lib/client/page.js',
|
|
790
|
+
versions: ['>=1.38.0']
|
|
791
|
+
}, (pagePackage) => {
|
|
792
|
+
shimmer.wrap(pagePackage.Page.prototype, 'goto', goto => async function (url, options) {
|
|
793
|
+
const response = await goto.apply(this, arguments)
|
|
794
|
+
|
|
795
|
+
const page = this
|
|
796
|
+
|
|
797
|
+
const isRumActive = await page.evaluate(() => {
|
|
798
|
+
if (window.DD_RUM && window.DD_RUM.getInternalContext) {
|
|
799
|
+
return !!window.DD_RUM.getInternalContext()
|
|
800
|
+
} else {
|
|
801
|
+
return false
|
|
802
|
+
}
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
if (isRumActive) {
|
|
806
|
+
testPageGotoCh.publish({
|
|
807
|
+
isRumActive,
|
|
808
|
+
page
|
|
809
|
+
})
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return response
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
return pagePackage
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
// Only in worker
|
|
819
|
+
addHook({
|
|
820
|
+
name: 'playwright',
|
|
821
|
+
file: 'lib/worker/workerMain.js',
|
|
822
|
+
versions: ['>=1.38.0']
|
|
823
|
+
}, (workerPackage) => {
|
|
824
|
+
// we assume there's only a test running at a time
|
|
825
|
+
let steps = []
|
|
826
|
+
const stepInfoByStepId = {}
|
|
827
|
+
|
|
828
|
+
shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
|
|
829
|
+
steps = []
|
|
830
|
+
|
|
831
|
+
const {
|
|
832
|
+
_requireFile: testSuiteAbsolutePath,
|
|
833
|
+
location: {
|
|
834
|
+
line: testSourceLine
|
|
835
|
+
}
|
|
836
|
+
} = test
|
|
837
|
+
let res
|
|
838
|
+
|
|
839
|
+
let testInfo
|
|
840
|
+
const testName = getTestFullname(test)
|
|
841
|
+
const browserName = this._project.project.name
|
|
842
|
+
|
|
843
|
+
// If test events are created in the worker process I need to stop creating it in the main process
|
|
844
|
+
// Probably yet another test worker exporter is needed in addition to the ones for mocha, jest and cucumber
|
|
845
|
+
// it's probably hard to tell that's a playwright worker though, as I don't think there is a specific env variable
|
|
846
|
+
const testAsyncResource = new AsyncResource('bound-anonymous-fn')
|
|
847
|
+
// TODO - In the future we may need to implement a mechanism to send test properties
|
|
848
|
+
// to the worker process before _runTest is called
|
|
849
|
+
testAsyncResource.runInAsyncScope(() => {
|
|
850
|
+
testStartCh.publish({
|
|
851
|
+
testName,
|
|
852
|
+
testSuiteAbsolutePath,
|
|
853
|
+
testSourceLine,
|
|
854
|
+
browserName
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
let existAfterEachHook = false
|
|
858
|
+
|
|
859
|
+
// We try to find an existing afterEach hook with _ddHook to avoid adding a new one
|
|
860
|
+
for (const hook of test.parent._hooks) {
|
|
861
|
+
if (hook.type === 'afterEach' && hook._ddHook) {
|
|
862
|
+
existAfterEachHook = true
|
|
863
|
+
break
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// In cases where there is no afterEach hook with _ddHook, we need to add one
|
|
868
|
+
if (!existAfterEachHook) {
|
|
869
|
+
test.parent._hooks.push({
|
|
870
|
+
type: 'afterEach',
|
|
871
|
+
fn: async function ({ page }) {
|
|
872
|
+
try {
|
|
873
|
+
if (page) {
|
|
874
|
+
const isRumActive = await page.evaluate(() => {
|
|
875
|
+
if (window.DD_RUM && window.DD_RUM.stopSession) {
|
|
876
|
+
window.DD_RUM.stopSession()
|
|
877
|
+
return true
|
|
878
|
+
} else {
|
|
879
|
+
return false
|
|
880
|
+
}
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
if (isRumActive) {
|
|
884
|
+
const url = page.url()
|
|
885
|
+
if (url) {
|
|
886
|
+
const domain = new URL(url).hostname
|
|
887
|
+
await page.context().addCookies([{
|
|
888
|
+
name: 'datadog-ci-visibility-test-execution-id',
|
|
889
|
+
value: '',
|
|
890
|
+
domain,
|
|
891
|
+
expires: 0,
|
|
892
|
+
path: '/'
|
|
893
|
+
}])
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
} catch (e) {
|
|
898
|
+
// ignore errors
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
title: 'afterEach hook',
|
|
902
|
+
_ddHook: true
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
res = _runTest.apply(this, arguments)
|
|
907
|
+
|
|
908
|
+
testInfo = this._currentTest
|
|
909
|
+
})
|
|
910
|
+
await res
|
|
911
|
+
|
|
912
|
+
const { status, error, annotations, retry, testId } = testInfo
|
|
913
|
+
|
|
914
|
+
// testInfo.errors could be better than "error",
|
|
915
|
+
// which will only include timeout error (even though the test failed because of a different error)
|
|
916
|
+
|
|
917
|
+
let annotationTags
|
|
918
|
+
if (annotations.length) {
|
|
919
|
+
annotationTags = parseAnnotations(annotations)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
let onDone
|
|
923
|
+
|
|
924
|
+
const flushPromise = new Promise(resolve => {
|
|
925
|
+
onDone = resolve
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
// Wait for ddProperties to be received and processed
|
|
929
|
+
// Create a promise that will be resolved when the properties are received
|
|
930
|
+
const ddPropertiesPromise = new Promise(resolve => {
|
|
931
|
+
const messageHandler = ({ type, testId, properties }) => {
|
|
932
|
+
if (type === 'ddProperties' && testId === test.id) {
|
|
933
|
+
// Apply the properties to the test object
|
|
934
|
+
if (properties) {
|
|
935
|
+
Object.assign(test, properties)
|
|
936
|
+
}
|
|
937
|
+
process.removeListener('message', messageHandler)
|
|
938
|
+
resolve()
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Add the listener
|
|
943
|
+
process.on('message', messageHandler)
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
// Wait for the properties to be received
|
|
947
|
+
await ddPropertiesPromise
|
|
948
|
+
|
|
949
|
+
testAsyncResource.runInAsyncScope(() => {
|
|
950
|
+
testFinishCh.publish({
|
|
951
|
+
testStatus: STATUS_TO_TEST_STATUS[status],
|
|
952
|
+
steps: steps.filter(step => step.testId === testId),
|
|
953
|
+
error,
|
|
954
|
+
extraTags: annotationTags,
|
|
955
|
+
isNew: test._ddIsNew,
|
|
956
|
+
isRetry: retry > 0,
|
|
957
|
+
isEfdRetry: test._ddIsEfdRetry,
|
|
958
|
+
isAttemptToFix: test._ddIsAttemptToFix,
|
|
959
|
+
isDisabled: test._ddIsDisabled,
|
|
960
|
+
isQuarantined: test._ddIsQuarantined,
|
|
961
|
+
isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
|
|
962
|
+
hasFailedAllRetries: test._ddHasFailedAllRetries,
|
|
963
|
+
hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
|
|
964
|
+
onDone
|
|
965
|
+
})
|
|
966
|
+
})
|
|
967
|
+
|
|
968
|
+
await flushPromise
|
|
969
|
+
|
|
970
|
+
return res
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
// We reproduce what happens in `Dispatcher#_onStepBegin` and `Dispatcher#_onStepEnd`,
|
|
974
|
+
// since `startTime` and `duration` are not available directly in the worker process
|
|
975
|
+
shimmer.wrap(workerPackage.WorkerMain.prototype, 'dispatchEvent', dispatchEvent => function (event, payload) {
|
|
976
|
+
if (event === 'stepBegin') {
|
|
977
|
+
stepInfoByStepId[payload.stepId] = {
|
|
978
|
+
startTime: payload.wallTime,
|
|
979
|
+
title: payload.title,
|
|
980
|
+
testId: payload.testId
|
|
981
|
+
}
|
|
982
|
+
} else if (event === 'stepEnd') {
|
|
983
|
+
const stepInfo = stepInfoByStepId[payload.stepId]
|
|
984
|
+
delete stepInfoByStepId[payload.stepId]
|
|
985
|
+
steps.push({
|
|
986
|
+
testId: stepInfo.testId,
|
|
987
|
+
startTime: new Date(stepInfo.startTime),
|
|
988
|
+
title: stepInfo.title,
|
|
989
|
+
duration: payload.wallTime - stepInfo.startTime,
|
|
990
|
+
error: payload.error
|
|
991
|
+
})
|
|
992
|
+
}
|
|
993
|
+
return dispatchEvent.apply(this, arguments)
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
return workerPackage
|
|
997
|
+
})
|
|
@@ -19,7 +19,7 @@ function createWrapRouterMethod (name) {
|
|
|
19
19
|
function wrapLayerHandle (layer, original) {
|
|
20
20
|
original._name = original._name || layer.name
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
return shimmer.wrapFunction(original, original => function () {
|
|
23
23
|
if (!enterChannel.hasSubscribers) return original.apply(this, arguments)
|
|
24
24
|
|
|
25
25
|
const matchers = layerMatchers.get(layer)
|
|
@@ -59,12 +59,6 @@ function createWrapRouterMethod (name) {
|
|
|
59
59
|
exitChannel.publish({ req })
|
|
60
60
|
}
|
|
61
61
|
})
|
|
62
|
-
|
|
63
|
-
// This is a workaround for the `loopback` library so that it can find the correct express layer
|
|
64
|
-
// that contains the real handle function
|
|
65
|
-
handle._datadog_orig = original
|
|
66
|
-
|
|
67
|
-
return handle
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
function wrapStack (stack, offset, matchers) {
|