dd-trace 5.74.0 → 5.76.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 (38) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/init.js +1 -0
  3. package/index.d.ts +11 -7
  4. package/loader-hook.mjs +52 -1
  5. package/package.json +7 -18
  6. package/packages/datadog-esbuild/index.js +1 -0
  7. package/packages/datadog-instrumentations/src/cookie-parser.js +0 -2
  8. package/packages/datadog-instrumentations/src/cucumber.js +2 -2
  9. package/packages/datadog-instrumentations/src/express-session.js +0 -1
  10. package/packages/datadog-instrumentations/src/mariadb.js +9 -7
  11. package/packages/datadog-instrumentations/src/openai.js +8 -0
  12. package/packages/datadog-instrumentations/src/playwright.js +163 -37
  13. package/packages/datadog-instrumentations/src/vitest.js +138 -20
  14. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -1
  15. package/packages/datadog-plugin-openai/src/stream-helpers.js +26 -1
  16. package/packages/datadog-plugin-openai/src/tracing.js +46 -1
  17. package/packages/datadog-plugin-playwright/src/index.js +74 -31
  18. package/packages/datadog-plugin-vitest/src/index.js +5 -1
  19. package/packages/datadog-shimmer/src/shimmer.js +2 -0
  20. package/packages/dd-trace/src/aiguard/sdk.js +25 -3
  21. package/packages/dd-trace/src/aiguard/tags.js +4 -1
  22. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +6 -0
  23. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +30 -7
  24. package/packages/dd-trace/src/config-helper.js +1 -0
  25. package/packages/dd-trace/src/config.js +345 -329
  26. package/packages/dd-trace/src/config_defaults.js +9 -0
  27. package/packages/dd-trace/src/llmobs/plugins/openai.js +216 -12
  28. package/packages/dd-trace/src/llmobs/span_processor.js +2 -1
  29. package/packages/dd-trace/src/llmobs/tagger.js +9 -3
  30. package/packages/dd-trace/src/plugins/ci_plugin.js +3 -1
  31. package/packages/dd-trace/src/plugins/util/test.js +6 -0
  32. package/packages/dd-trace/src/profiling/config.js +32 -10
  33. package/packages/dd-trace/src/proxy.js +1 -1
  34. package/packages/dd-trace/src/remote_config/capabilities.js +2 -0
  35. package/packages/dd-trace/src/remote_config/index.js +4 -0
  36. package/packages/dd-trace/src/supported-configurations.json +1 -0
  37. package/packages/dd-trace/src/telemetry/logs/log-collector.js +5 -3
  38. package/register.js +1 -11
@@ -14,6 +14,7 @@ require,@opentelemetry/resources,Apache license 2.0,Copyright OpenTelemetry Auth
14
14
  require,@isaacs/ttlcache,ISC,Copyright (c) 2022-2023 - Isaac Z. Schlueter and Contributors
15
15
  require,crypto-randomuuid,MIT,Copyright 2021 Node.js Foundation and contributors
16
16
  require,dc-polyfill,MIT,Copyright 2023 Datadog Inc.
17
+ require,escape-string-regexp,MIT,Copyright Sindre Sorhus
17
18
  require,ignore,MIT,Copyright 2013 Kael Zhang and contributors
18
19
  require,import-in-the-middle,Apache license 2.0,Copyright 2021 Datadog Inc.
19
20
  require,istanbul-lib-coverage,BSD-3-Clause,Copyright 2012-2015 Yahoo! Inc.
package/ci/init.js CHANGED
@@ -29,6 +29,7 @@ function detectTestWorkerType () {
29
29
  if (getEnvironmentVariable('MOCHA_WORKER_ID')) return 'mocha'
30
30
  if (getEnvironmentVariable('DD_PLAYWRIGHT_WORKER')) return 'playwright'
31
31
  if (getEnvironmentVariable('TINYPOOL_WORKER_ID')) return 'vitest'
32
+ if (getEnvironmentVariable('DD_VITEST_WORKER')) return 'vitest'
32
33
  return null
33
34
  }
34
35
 
package/index.d.ts CHANGED
@@ -181,6 +181,7 @@ interface Tracer extends opentracing.Tracer {
181
181
  /** @hidden */
182
182
  interface Plugins {
183
183
  "aerospike": tracer.plugins.aerospike;
184
+ "ai": tracer.plugins.ai;
184
185
  "amqp10": tracer.plugins.amqp10;
185
186
  "amqplib": tracer.plugins.amqplib;
186
187
  "anthropic": tracer.plugins.anthropic;
@@ -1642,6 +1643,12 @@ declare namespace tracer {
1642
1643
  */
1643
1644
  interface aerospike extends Instrumentation {}
1644
1645
 
1646
+ /**
1647
+ * This plugin automatically instruments the
1648
+ * [Vercel AI SDK](https://ai-sdk.dev/docs/introduction) module.
1649
+ */
1650
+ interface ai extends Instrumentation {}
1651
+
1645
1652
  /**
1646
1653
  * This plugin automatically instruments the
1647
1654
  * [amqp10](https://github.com/noodlefrenzy/node-amqp10) module.
@@ -1698,12 +1705,6 @@ declare namespace tracer {
1698
1705
  * [aws-sdk](https://github.com/aws/aws-sdk-js) module.
1699
1706
  */
1700
1707
  interface aws_sdk extends Instrumentation {
1701
- /**
1702
- * Whether to add a suffix to the service name so that each AWS service has its own service name.
1703
- * @default true
1704
- */
1705
- splitByAwsService?: boolean;
1706
-
1707
1708
  /**
1708
1709
  * Whether to inject all messages during batch AWS SQS, Kinesis, and SNS send operations. Normal
1709
1710
  * behavior is to inject the first message in batch send operations.
@@ -2821,7 +2822,10 @@ declare namespace tracer {
2821
2822
  redactionValuePattern?: string,
2822
2823
 
2823
2824
  /**
2824
- * Allows to enable security controls.
2825
+ * Allows to enable security controls. This option is not supported when
2826
+ * using ESM.
2827
+ * @deprecated Please use the DD_IAST_SECURITY_CONTROLS_CONFIGURATION
2828
+ * environment variable instead.
2825
2829
  */
2826
2830
  securityControlsConfiguration?: string,
2827
2831
 
package/loader-hook.mjs CHANGED
@@ -1 +1,52 @@
1
- export * from 'import-in-the-middle/hook.mjs'
1
+ import regexpEscape from 'escape-string-regexp'
2
+ import * as iitm from 'import-in-the-middle/hook.mjs'
3
+ import hooks from './packages/datadog-instrumentations/src/helpers/hooks.js'
4
+ import configHelper from './packages/dd-trace/src/config-helper.js'
5
+
6
+ // For some reason `getEnvironmentVariable` is not otherwise available to ESM.
7
+ const env = configHelper.getEnvironmentVariable
8
+
9
+ function initialize (data = {}) {
10
+ data.include ??= []
11
+ data.exclude ??= []
12
+
13
+ addInstrumentations(data)
14
+ addSecurityControls(data)
15
+ addExclusions(data)
16
+
17
+ return iitm.initialize(data)
18
+ }
19
+
20
+ function addInstrumentations (data) {
21
+ const instrumentations = Object.keys(hooks)
22
+
23
+ for (const moduleName of instrumentations) {
24
+ data.include.push(new RegExp(`node_modules/${moduleName}/(?!node_modules).+`), moduleName)
25
+ }
26
+ }
27
+
28
+ function addSecurityControls (data) {
29
+ const securityControls = (env('DD_IAST_SECURITY_CONTROLS_CONFIGURATION') || '')
30
+ .split(';')
31
+ .map(sc => sc.trim().split(':')[2])
32
+ .filter(Boolean)
33
+ .map(sc => sc.trim())
34
+
35
+ for (const subpath of securityControls) {
36
+ data.include.push(new RegExp(regexpEscape(subpath)))
37
+ }
38
+ }
39
+
40
+ function addExclusions (data) {
41
+ data.exclude.push(
42
+ /middle/,
43
+ /langsmith/,
44
+ /openai\/_shims/,
45
+ /openai\/resources\/chat\/completions\/messages/,
46
+ /openai\/agents-core\/dist\/shims/,
47
+ /@anthropic-ai\/sdk\/_shims/
48
+ )
49
+ }
50
+
51
+ export { initialize }
52
+ export { load, getFormat, resolve, getSource } from 'import-in-the-middle/hook.mjs'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.74.0",
3
+ "version": "5.76.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -125,8 +125,8 @@
125
125
  "@datadog/native-appsec": "10.3.0",
126
126
  "@datadog/native-iast-taint-tracking": "4.0.0",
127
127
  "@datadog/native-metrics": "3.1.1",
128
- "@datadog/openfeature-node-server": "0.1.0-preview.12",
129
- "@datadog/pprof": "5.11.1",
128
+ "@datadog/openfeature-node-server": "0.1.0-preview.13",
129
+ "@datadog/pprof": "5.12.0",
130
130
  "@datadog/sketches-js": "2.1.1",
131
131
  "@datadog/wasm-js-rewriter": "4.0.1",
132
132
  "@isaacs/ttlcache": "^1.4.1",
@@ -136,6 +136,7 @@
136
136
  "@opentelemetry/resources": ">=1.0.0 <1.10.0",
137
137
  "crypto-randomuuid": "^1.0.0",
138
138
  "dc-polyfill": "^0.1.10",
139
+ "escape-string-regexp": "^5.0.0",
139
140
  "ignore": "^7.0.5",
140
141
  "import-in-the-middle": "^1.14.2",
141
142
  "istanbul-lib-coverage": "^3.2.2",
@@ -158,25 +159,13 @@
158
159
  "tlhunter-sorted-set": "^0.1.0",
159
160
  "ttl-set": "^1.0.0"
160
161
  },
161
- "peerDependencies": {
162
- "@openfeature/core": "^1.9.0",
163
- "@openfeature/server-sdk": "~1.20.0"
164
- },
165
- "peerDependenciesMeta": {
166
- "@openfeature/core": {
167
- "optional": true
168
- },
169
- "@openfeature/server-sdk": {
170
- "optional": true
171
- }
172
- },
173
162
  "devDependencies": {
174
163
  "@babel/helpers": "^7.27.6",
175
164
  "@eslint/eslintrc": "^3.3.1",
176
165
  "@eslint/js": "^9.29.0",
177
166
  "@msgpack/msgpack": "^3.1.2",
178
- "@openfeature/core": "^1.8.1",
179
- "@openfeature/server-sdk": "~1.20.0",
167
+ "@openfeature/core": "^1.9.0",
168
+ "@openfeature/server-sdk": "^1.20.0",
180
169
  "@stylistic/eslint-plugin": "^5.0.0",
181
170
  "@types/chai": "^4.3.16",
182
171
  "@types/mocha": "^10.0.10",
@@ -214,7 +203,7 @@
214
203
  "tap": "^16.3.10",
215
204
  "tiktoken": "^1.0.21",
216
205
  "typescript": "^5.9.2",
217
- "workerpool": "^9.2.0",
206
+ "workerpool": "^10.0.0",
218
207
  "yaml": "^2.8.0",
219
208
  "yarn-deduplicate": "^6.0.2"
220
209
  }
@@ -114,6 +114,7 @@ ${build.initialOptions.banner.js}`
114
114
  }
115
115
 
116
116
  try {
117
+ // eslint-disable-next-line n/no-unpublished-require
117
118
  require.resolve('@openfeature/core')
118
119
  } catch (error) {
119
120
  build.initialOptions.external ??= []
@@ -25,8 +25,6 @@ addHook({
25
25
  name: 'cookie-parser',
26
26
  versions: ['>=1.0.0']
27
27
  }, cookieParser => {
28
- // This prevents the non default export from entering the wrapping process
29
- if (cookieParser.default) return cookieParser
30
28
  return shimmer.wrapFunction(cookieParser, cookieParser => function () {
31
29
  const cookieMiddleware = cookieParser.apply(this, arguments)
32
30
 
@@ -259,7 +259,7 @@ function wrapRun (pl, isLatestVersion, version) {
259
259
  testStartCh.runStores(ctx, () => {})
260
260
  const promises = {}
261
261
  try {
262
- this.eventBroadcaster.on('envelope', shimmer.wrapFunction(null, () => async (testCase) => {
262
+ this.eventBroadcaster.on('envelope', async (testCase) => {
263
263
  // Only supported from >=8.0.0
264
264
  if (testCase?.testCaseFinished) {
265
265
  const { testCaseFinished: { willBeRetried } } = testCase
@@ -289,7 +289,7 @@ function wrapRun (pl, isLatestVersion, version) {
289
289
  testStartCh.runStores(newCtx, () => {})
290
290
  }
291
291
  }
292
- }))
292
+ })
293
293
  let promise
294
294
 
295
295
  testFnCh.runStores(ctx, () => {
@@ -37,6 +37,5 @@ addHook({
37
37
  name: 'express-session',
38
38
  versions: ['>=1.5.0']
39
39
  }, session => {
40
- if (session.default) return session
41
40
  return shimmer.wrapFunction(session, wrapSession)
42
41
  })
@@ -82,12 +82,7 @@ function createWrapQueryCallback (options) {
82
82
 
83
83
  const cb = arguments[arguments.length - 1]
84
84
  const ctx = { sql, conf: options }
85
-
86
- if (typeof cb !== 'function') {
87
- arguments.length += 1
88
- }
89
-
90
- arguments[arguments.length - 1] = shimmer.wrapFunction(cb, cb => function (err) {
85
+ const wrapper = (cb) => function (err) {
91
86
  if (err) {
92
87
  ctx.error = err
93
88
  errorCh.publish(ctx)
@@ -96,7 +91,14 @@ function createWrapQueryCallback (options) {
96
91
  return typeof cb === 'function'
97
92
  ? finishCh.runStores(ctx, cb, this, ...arguments)
98
93
  : finishCh.publish(ctx)
99
- })
94
+ }
95
+
96
+ if (typeof cb === 'function') {
97
+ arguments[arguments.length - 1] = shimmer.wrapFunction(cb, wrapper)
98
+ } else {
99
+ arguments.length += 1
100
+ arguments[arguments.length - 1] = wrapper()
101
+ }
100
102
 
101
103
  return startCh.runStores(ctx, query, this, ...arguments)
102
104
  }
@@ -22,6 +22,14 @@ const V4_PACKAGE_SHIMS = [
22
22
  methods: ['create'],
23
23
  streamedResponse: true
24
24
  },
25
+ {
26
+ file: 'resources/responses/responses',
27
+ targetClass: 'Responses',
28
+ baseResource: 'responses',
29
+ methods: ['create'],
30
+ streamedResponse: true,
31
+ versions: ['>=4.87.0']
32
+ },
25
33
  {
26
34
  file: 'resources/embeddings',
27
35
  targetClass: 'Embeddings',
@@ -15,6 +15,7 @@ const { DD_MAJOR } = require('../../../version')
15
15
 
16
16
  const testStartCh = channel('ci:playwright:test:start')
17
17
  const testFinishCh = channel('ci:playwright:test:finish')
18
+ const testSkipCh = channel('ci:playwright:test:skip')
18
19
 
19
20
  const testSessionStartCh = channel('ci:playwright:session:start')
20
21
  const testSessionFinishCh = channel('ci:playwright:session:finish')
@@ -37,6 +38,8 @@ const testSuiteToTestStatuses = new Map()
37
38
  const testSuiteToErrors = new Map()
38
39
  const testsToTestStatuses = new Map()
39
40
 
41
+ const RUM_FLUSH_WAIT_TIME = 1000
42
+
40
43
  let applyRepeatEachIndex = null
41
44
 
42
45
  let startedSuites = []
@@ -65,6 +68,8 @@ let modifiedFiles = {}
65
68
  const quarantinedOrDisabledTestsAttemptToFix = []
66
69
  let quarantinedButNotAttemptToFixFqns = new Set()
67
70
  let rootDir = ''
71
+ let sessionProjects = []
72
+
68
73
  const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5
69
74
 
70
75
  function isValidKnownTests (receivedKnownTests) {
@@ -285,7 +290,12 @@ function getTestFullname (test) {
285
290
  return names.join(' ')
286
291
  }
287
292
 
288
- function testBeginHandler (test, browserName, isMainProcess) {
293
+ function shouldFinishTestSuite (testSuiteAbsolutePath) {
294
+ const remainingTests = remainingTestsByFile[testSuiteAbsolutePath]
295
+ return !remainingTests.length || remainingTests.every(test => test.expectedStatus === 'skipped')
296
+ }
297
+
298
+ function testBeginHandler (test, browserName, shouldCreateTestSpan) {
289
299
  const {
290
300
  _requireFile: testSuiteAbsolutePath,
291
301
  location: {
@@ -297,6 +307,10 @@ function testBeginHandler (test, browserName, isMainProcess) {
297
307
  if (_type === 'beforeAll' || _type === 'afterAll') {
298
308
  return
299
309
  }
310
+ // this means that a skipped test is being handled
311
+ if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
312
+ return
313
+ }
300
314
 
301
315
  const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath)
302
316
 
@@ -313,7 +327,7 @@ function testBeginHandler (test, browserName, isMainProcess) {
313
327
  }
314
328
 
315
329
  // this handles tests that do not go through the worker process (because they're skipped)
316
- if (isMainProcess) {
330
+ if (shouldCreateTestSpan) {
317
331
  const testName = getTestFullname(test)
318
332
  const testCtx = {
319
333
  testName,
@@ -328,8 +342,20 @@ function testBeginHandler (test, browserName, isMainProcess) {
328
342
  }
329
343
  }
330
344
 
331
- function testEndHandler (test, annotations, testStatus, error, isTimeout, isMainProcess) {
332
- const { _requireFile: testSuiteAbsolutePath, results, _type } = test
345
+ function testEndHandler ({
346
+ test,
347
+ annotations,
348
+ testStatus,
349
+ error,
350
+ isTimeout,
351
+ shouldCreateTestSpan,
352
+ projects
353
+ }) {
354
+ const {
355
+ _requireFile: testSuiteAbsolutePath,
356
+ results,
357
+ _type,
358
+ } = test
333
359
 
334
360
  let annotationTags
335
361
  if (annotations.length) {
@@ -368,31 +394,34 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
368
394
  }
369
395
 
370
396
  // this handles tests that do not go through the worker process (because they're skipped)
371
- if (isMainProcess) {
397
+ if (shouldCreateTestSpan) {
372
398
  const testResult = results.at(-1)
373
399
  const testCtx = testToCtx.get(test)
374
400
  const isAtrRetry = testResult?.retry > 0 &&
375
401
  isFlakyTestRetriesEnabled &&
376
402
  !test._ddIsAttemptToFix &&
377
403
  !test._ddIsEfdRetry
378
- testFinishCh.publish({
379
- testStatus,
380
- steps: testResult?.steps || [],
381
- isRetry: testResult?.retry > 0,
382
- error,
383
- extraTags: annotationTags,
384
- isNew: test._ddIsNew,
385
- isAttemptToFix: test._ddIsAttemptToFix,
386
- isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
387
- isQuarantined: test._ddIsQuarantined,
388
- isEfdRetry: test._ddIsEfdRetry,
389
- hasFailedAllRetries: test._ddHasFailedAllRetries,
390
- hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
391
- hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
392
- isAtrRetry,
393
- isModified: test._ddIsModified,
394
- ...testCtx.currentStore
395
- })
404
+ // if there is no testCtx, the skipped test will be created later
405
+ if (testCtx) {
406
+ testFinishCh.publish({
407
+ testStatus,
408
+ steps: testResult?.steps || [],
409
+ isRetry: testResult?.retry > 0,
410
+ error,
411
+ extraTags: annotationTags,
412
+ isNew: test._ddIsNew,
413
+ isAttemptToFix: test._ddIsAttemptToFix,
414
+ isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
415
+ isQuarantined: test._ddIsQuarantined,
416
+ isEfdRetry: test._ddIsEfdRetry,
417
+ hasFailedAllRetries: test._ddHasFailedAllRetries,
418
+ hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
419
+ hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
420
+ isAtrRetry,
421
+ isModified: test._ddIsModified,
422
+ ...testCtx.currentStore
423
+ })
424
+ }
396
425
  }
397
426
 
398
427
  if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
@@ -410,8 +439,25 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
410
439
  .filter(currentTest => currentTest !== test)
411
440
  }
412
441
 
413
- // Last test, we finish the suite
414
- if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
442
+ if (shouldFinishTestSuite(testSuiteAbsolutePath)) {
443
+ const skippedTests = remainingTestsByFile[testSuiteAbsolutePath]
444
+ .filter(test => test.expectedStatus === 'skipped')
445
+
446
+ for (const test of skippedTests) {
447
+ const browserName = getBrowserNameFromProjects(projects, test)
448
+ testSkipCh.publish({
449
+ testName: getTestFullname(test),
450
+ testSuiteAbsolutePath,
451
+ testSourceLine: test.location.line,
452
+ browserName,
453
+ isNew: test._ddIsNew,
454
+ isDisabled: test._ddIsDisabled,
455
+ isModified: test._ddIsModified,
456
+ isQuarantined: test._ddIsQuarantined
457
+ })
458
+ }
459
+ remainingTestsByFile[testSuiteAbsolutePath] = []
460
+
415
461
  const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
416
462
  let testSuiteStatus = 'pass'
417
463
  if (testStatuses.includes('fail')) {
@@ -450,10 +496,14 @@ function dispatcherHook (dispatcherExport) {
450
496
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
451
497
  const dispatcher = this
452
498
  const worker = createWorker.apply(this, arguments)
499
+ const projects = getProjectsFromDispatcher(dispatcher)
500
+ sessionProjects = projects
501
+
502
+ // for older versions of playwright, `shouldCreateTestSpan` should always be true,
503
+ // since the `_runTest` function wrapper is not available for older versions
453
504
  worker.process.on('message', ({ method, params }) => {
454
505
  if (method === 'testBegin') {
455
506
  const { test } = dispatcher._testById.get(params.testId)
456
- const projects = getProjectsFromDispatcher(dispatcher)
457
507
  const browser = getBrowserNameFromProjects(projects, test)
458
508
  testBeginHandler(test, browser, true)
459
509
  } else if (method === 'testEnd') {
@@ -464,12 +514,15 @@ function dispatcherHook (dispatcherExport) {
464
514
 
465
515
  const isTimeout = testResult.status === 'timedOut'
466
516
  testEndHandler(
467
- test,
468
- params.annotations,
469
- STATUS_TO_TEST_STATUS[testResult.status],
470
- testResult.error,
471
- isTimeout,
472
- true
517
+ {
518
+ test,
519
+ annotations: params.annotations,
520
+ testStatus: STATUS_TO_TEST_STATUS[testResult.status],
521
+ error: testResult.error,
522
+ isTimeout,
523
+ shouldCreateTestSpan: true,
524
+ projects
525
+ }
473
526
  )
474
527
  }
475
528
  })
@@ -484,18 +537,31 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
484
537
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
485
538
  const dispatcher = this
486
539
  const worker = createWorker.apply(this, arguments)
540
+ const projects = getProjectsFromDispatcher(dispatcher)
541
+ sessionProjects = projects
487
542
 
488
543
  worker.on('testBegin', ({ testId }) => {
489
544
  const test = getTestByTestId(dispatcher, testId)
490
- const projects = getProjectsFromDispatcher(dispatcher)
491
545
  const browser = getBrowserNameFromProjects(projects, test)
492
- testBeginHandler(test, browser, false)
546
+ const shouldCreateTestSpan = test.expectedStatus === 'skipped'
547
+ testBeginHandler(test, browser, shouldCreateTestSpan)
493
548
  })
494
549
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
495
550
  const test = getTestByTestId(dispatcher, testId)
496
551
 
497
552
  const isTimeout = status === 'timedOut'
498
- testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout, false)
553
+ const shouldCreateTestSpan = test.expectedStatus === 'skipped'
554
+ testEndHandler(
555
+ {
556
+ test,
557
+ annotations,
558
+ testStatus: STATUS_TO_TEST_STATUS[status],
559
+ error: errors && errors[0],
560
+ isTimeout,
561
+ shouldCreateTestSpan,
562
+ projects
563
+ }
564
+ )
499
565
  const testResult = test.results.at(-1)
500
566
  const isAtrRetry = testResult?.retry > 0 &&
501
567
  isFlakyTestRetriesEnabled &&
@@ -625,6 +691,9 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
625
691
 
626
692
  let runAllTestsReturn = await runAllTests.apply(this, arguments)
627
693
 
694
+ // Tests that have only skipped tests may reach this point
695
+ // Skipped tests may or may not go through `testBegin` or `testEnd`
696
+ // depending on the playwright configuration
628
697
  Object.values(remainingTestsByFile).forEach(tests => {
629
698
  // `tests` should normally be empty, but if it isn't,
630
699
  // there were tests that did not go through `testBegin` or `testEnd`,
@@ -632,7 +701,15 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
632
701
  tests.forEach(test => {
633
702
  const browser = getBrowserNameFromProjects(projects, test)
634
703
  testBeginHandler(test, browser, true)
635
- testEndHandler(test, [], 'skip', null, false, true)
704
+ testEndHandler({
705
+ test,
706
+ annotations: [],
707
+ testStatus: 'skip',
708
+ error: null,
709
+ isTimeout: false,
710
+ shouldCreateTestSpan: true,
711
+ projects
712
+ })
636
713
  })
637
714
  })
638
715
 
@@ -1007,6 +1084,9 @@ addHook({
1007
1084
  const stepInfoByStepId = {}
1008
1085
 
1009
1086
  shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
1087
+ if (test.expectedStatus === 'skipped') {
1088
+ return _runTest.apply(this, arguments)
1089
+ }
1010
1090
  steps = []
1011
1091
 
1012
1092
  const {
@@ -1060,6 +1140,8 @@ addHook({
1060
1140
  })
1061
1141
 
1062
1142
  if (isRumActive) {
1143
+ // Give some time RUM to flush data, similar to what we do in selenium
1144
+ await new Promise(resolve => setTimeout(resolve, RUM_FLUSH_WAIT_TIME))
1063
1145
  const url = page.url()
1064
1146
  if (url) {
1065
1147
  const domain = new URL(url).hostname
@@ -1067,9 +1149,10 @@ addHook({
1067
1149
  name: 'datadog-ci-visibility-test-execution-id',
1068
1150
  value: '',
1069
1151
  domain,
1070
- expires: 0,
1071
1152
  path: '/'
1072
1153
  }])
1154
+ } else {
1155
+ log.error('RUM is active but page.url() is not available')
1073
1156
  }
1074
1157
  }
1075
1158
  }
@@ -1176,3 +1259,46 @@ addHook({
1176
1259
 
1177
1260
  return workerPackage
1178
1261
  })
1262
+
1263
+ function generateSummaryWrapper (generateSummary) {
1264
+ return function () {
1265
+ for (const test of this.suite.allTests()) {
1266
+ // https://github.com/microsoft/playwright/blob/bf92ffecff6f30a292b53430dbaee0207e0c61ad/packages/playwright/src/reporters/base.ts#L279
1267
+ const didNotRun = test.outcome() === 'skipped' &&
1268
+ (!test.results.length || test.expectedStatus !== 'skipped')
1269
+ if (didNotRun) {
1270
+ const {
1271
+ _requireFile: testSuiteAbsolutePath,
1272
+ location: { line: testSourceLine },
1273
+ } = test
1274
+ const browserName = getBrowserNameFromProjects(sessionProjects, test)
1275
+
1276
+ testSkipCh.publish({
1277
+ testName: getTestFullname(test),
1278
+ testSuiteAbsolutePath,
1279
+ testSourceLine,
1280
+ browserName,
1281
+ })
1282
+ }
1283
+ }
1284
+ return generateSummary.apply(this, arguments)
1285
+ }
1286
+ }
1287
+
1288
+ // If a playwright project B has a dependency on project A,
1289
+ // and project A fails, the tests in project B will not run.
1290
+ // This hook is used to report tests that did not run as skipped.
1291
+ // Note: this is different from tests skipped via test.skip() or test.fixme()
1292
+ addHook({
1293
+ name: 'playwright',
1294
+ file: 'lib/reporters/base.js',
1295
+ versions: ['>=1.38.0']
1296
+ }, (reportersPackage) => {
1297
+ // v1.50.0 changed the name of the base reporter from BaseReporter to TerminalReporter
1298
+ if (reportersPackage.TerminalReporter) {
1299
+ shimmer.wrap(reportersPackage.TerminalReporter.prototype, 'generateSummary', generateSummaryWrapper)
1300
+ } else if (reportersPackage.BaseReporter) {
1301
+ shimmer.wrap(reportersPackage.BaseReporter.prototype, 'generateSummary', generateSummaryWrapper)
1302
+ }
1303
+ return reportersPackage
1304
+ })