dd-trace 5.44.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.
Files changed (62) hide show
  1. package/LICENSE-3rdparty.csv +1 -1
  2. package/ci/init.js +8 -0
  3. package/ext/exporters.d.ts +2 -1
  4. package/ext/exporters.js +2 -1
  5. package/package.json +3 -3
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  7. package/packages/datadog-instrumentations/src/helpers/register.js +41 -1
  8. package/packages/datadog-instrumentations/src/mariadb.js +19 -0
  9. package/packages/datadog-instrumentations/src/playwright.js +321 -46
  10. package/packages/datadog-instrumentations/src/router.js +1 -7
  11. package/packages/datadog-plugin-mongodb-core/src/index.js +20 -0
  12. package/packages/datadog-plugin-playwright/src/index.js +115 -8
  13. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +39 -15
  14. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +1 -1
  15. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -1
  16. package/packages/dd-trace/src/appsec/rasp/command_injection.js +1 -1
  17. package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
  18. package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
  19. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +1 -1
  20. package/packages/dd-trace/src/appsec/rasp/ssrf.js +1 -1
  21. package/packages/dd-trace/src/appsec/rasp/utils.js +12 -7
  22. package/packages/dd-trace/src/appsec/recommended.json +256 -84
  23. package/packages/dd-trace/src/appsec/reporter.js +6 -4
  24. package/packages/dd-trace/src/appsec/sdk/track_event.js +7 -0
  25. package/packages/dd-trace/src/appsec/telemetry/index.js +35 -4
  26. package/packages/dd-trace/src/appsec/telemetry/rasp.js +70 -6
  27. package/packages/dd-trace/src/appsec/telemetry/user.js +9 -1
  28. package/packages/dd-trace/src/appsec/telemetry/waf.js +0 -30
  29. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -0
  30. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +8 -3
  31. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +6 -4
  32. package/packages/dd-trace/src/constants.js +1 -0
  33. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +102 -22
  34. package/packages/dd-trace/src/debugger/devtools_client/condition.js +263 -0
  35. package/packages/dd-trace/src/debugger/devtools_client/index.js +69 -36
  36. package/packages/dd-trace/src/debugger/devtools_client/lock.js +8 -0
  37. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -7
  38. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -2
  39. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +15 -10
  40. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +3 -3
  41. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +69 -62
  42. package/packages/dd-trace/src/debugger/devtools_client/state.js +3 -2
  43. package/packages/dd-trace/src/debugger/index.js +3 -0
  44. package/packages/dd-trace/src/dogstatsd.js +94 -77
  45. package/packages/dd-trace/src/encode/0.4.js +24 -17
  46. package/packages/dd-trace/src/exporter.js +1 -0
  47. package/packages/dd-trace/src/format.js +58 -60
  48. package/packages/dd-trace/src/histogram.js +12 -23
  49. package/packages/dd-trace/src/llmobs/index.js +3 -0
  50. package/packages/dd-trace/src/llmobs/telemetry.js +27 -1
  51. package/packages/dd-trace/src/llmobs/writers/base.js +4 -0
  52. package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -3
  53. package/packages/dd-trace/src/opentelemetry/span.js +4 -4
  54. package/packages/dd-trace/src/plugin_manager.js +2 -0
  55. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  56. package/packages/dd-trace/src/profiler.js +1 -1
  57. package/packages/dd-trace/src/profiling/config.js +6 -0
  58. package/packages/dd-trace/src/profiling/profiler.js +4 -3
  59. package/packages/dd-trace/src/profiling/profilers/wall.js +10 -8
  60. package/packages/dd-trace/src/remote_config/manager.js +5 -0
  61. package/packages/dd-trace/src/tagger.js +38 -26
  62. package/packages/dd-trace/src/util.js +1 -7
@@ -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/native-iast-rewriter,Apache license 2.0,Copyright 2018 Datadog Inc.
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)
@@ -5,7 +5,8 @@ declare const exporters: {
5
5
  AGENT_PROXY: 'agent_proxy',
6
6
  JEST_WORKER: 'jest_worker',
7
7
  CUCUMBER_WORKER: 'cucumber_worker',
8
- MOCHA_WORKER: 'mocha_worker'
8
+ MOCHA_WORKER: 'mocha_worker',
9
+ PLAYWRIGHT_WORKER: 'playwright_worker'
9
10
  }
10
11
 
11
12
  export = exporters
package/ext/exporters.js CHANGED
@@ -6,5 +6,6 @@ module.exports = {
6
6
  AGENT_PROXY: 'agent_proxy',
7
7
  JEST_WORKER: 'jest_worker',
8
8
  CUCUMBER_WORKER: 'cucumber_worker',
9
- MOCHA_WORKER: 'mocha_worker'
9
+ MOCHA_WORKER: 'mocha_worker',
10
+ PLAYWRIGHT_WORKER: 'playwright_worker'
10
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.44.0",
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/native-iast-rewriter": "2.8.0",
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.6.0",
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 (!success && !seenCombo.has(nameVersion)) {
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 { parseAnnotations, getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
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
- const testAsyncResource = new AsyncResource('bound-anonymous-fn')
290
- testToAr.set(test, testAsyncResource)
291
- testAsyncResource.runInAsyncScope(() => {
292
- testStartCh.publish({
293
- testName,
294
- testSuiteAbsolutePath,
295
- testSourceLine,
296
- browserName,
297
- isDisabled: test._ddIsDisabled
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
- function testEndHandler (test, annotations, testStatus, error, isTimeout) {
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
- hasFailedAllRetries = true
341
+ test._ddHasFailedAllRetries = true
333
342
  } else if (testStatuses.every(status => status === 'pass')) {
334
- hasPassedAttemptToFixRetries = true
343
+ test._ddHasPassedAttemptToFixRetries = true
335
344
  }
336
345
  }
337
346
 
338
- const testResult = results[results.length - 1]
339
- const testAsyncResource = testToAr.get(test)
340
- testAsyncResource.runInAsyncScope(() => {
341
- testFinishCh.publish({
342
- testStatus,
343
- steps: testResult?.steps || [],
344
- isRetry: testResult?.retry > 0,
345
- error,
346
- extraTags: annotationTags,
347
- isNew: test._ddIsNew,
348
- isAttemptToFix: test._ddIsAttemptToFix,
349
- isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
350
- isQuarantined: test._ddIsQuarantined,
351
- isEfdRetry: test._ddIsEfdRetry,
352
- hasFailedAllRetries,
353
- hasPassedAttemptToFixRetries
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(test, params.annotations, STATUS_TO_TEST_STATUS[testResult.status], testResult.error, isTimeout)
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
- const handle = shimmer.wrapFunction(original, original => function () {
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) {