dd-trace 5.88.0 → 5.89.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 (59) hide show
  1. package/ext/tags.js +2 -0
  2. package/index.d.ts +9 -0
  3. package/package.json +12 -8
  4. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +26 -111
  5. package/packages/datadog-instrumentations/src/helpers/rewriter/{compiler.js → orchestrion/compiler.js} +5 -5
  6. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +43 -0
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +49 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +121 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/{transforms.js → orchestrion/transforms.js} +6 -6
  10. package/packages/datadog-instrumentations/src/jest.js +101 -43
  11. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +36 -5
  12. package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
  13. package/packages/datadog-plugin-cypress/src/support.js +4 -1
  14. package/packages/dd-trace/src/aiguard/sdk.js +5 -1
  15. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +1 -0
  16. package/packages/dd-trace/src/config/index.js +2 -0
  17. package/packages/dd-trace/src/config/supported-configurations.json +10 -0
  18. package/packages/dd-trace/src/datastreams/checkpointer.js +13 -0
  19. package/packages/dd-trace/src/datastreams/index.js +3 -0
  20. package/packages/dd-trace/src/datastreams/manager.js +9 -0
  21. package/packages/dd-trace/src/datastreams/processor.js +126 -3
  22. package/packages/dd-trace/src/encode/agentless-json.js +16 -2
  23. package/packages/dd-trace/src/exporters/agent/writer.js +7 -8
  24. package/packages/dd-trace/src/pkg.js +1 -1
  25. package/packages/dd-trace/src/proxy.js +2 -1
  26. package/packages/dd-trace/src/startup-log.js +52 -18
  27. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  28. package/vendor/dist/@datadog/source-map/index.js +1 -1
  29. package/vendor/dist/@isaacs/ttlcache/index.js +1 -1
  30. package/vendor/dist/@opentelemetry/core/index.js +1 -1
  31. package/vendor/dist/@opentelemetry/resources/index.js +1 -1
  32. package/vendor/dist/astring/index.js +1 -1
  33. package/vendor/dist/crypto-randomuuid/index.js +1 -1
  34. package/vendor/dist/escape-string-regexp/index.js +1 -1
  35. package/vendor/dist/esquery/index.js +1 -1
  36. package/vendor/dist/ignore/index.js +1 -1
  37. package/vendor/dist/istanbul-lib-coverage/index.js +1 -1
  38. package/vendor/dist/jest-docblock/index.js +1 -1
  39. package/vendor/dist/jsonpath-plus/index.js +1 -1
  40. package/vendor/dist/limiter/index.js +1 -1
  41. package/vendor/dist/lodash.sortby/index.js +1 -1
  42. package/vendor/dist/lru-cache/index.js +1 -1
  43. package/vendor/dist/meriyah/index.js +1 -1
  44. package/vendor/dist/module-details-from-path/index.js +1 -1
  45. package/vendor/dist/mutexify/promise/index.js +1 -1
  46. package/vendor/dist/opentracing/index.js +1 -1
  47. package/vendor/dist/path-to-regexp/index.js +1 -1
  48. package/vendor/dist/pprof-format/index.js +1 -1
  49. package/vendor/dist/protobufjs/index.js +1 -1
  50. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  51. package/vendor/dist/retry/index.js +1 -1
  52. package/vendor/dist/rfdc/index.js +1 -1
  53. package/vendor/dist/semifies/index.js +1 -1
  54. package/vendor/dist/shell-quote/index.js +1 -1
  55. package/vendor/dist/source-map/index.js +1 -1
  56. package/vendor/dist/source-map/lib/util/index.js +1 -1
  57. package/vendor/dist/tlhunter-sorted-set/index.js +1 -1
  58. package/vendor/dist/ttl-set/index.js +1 -1
  59. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +0 -21
@@ -69,7 +69,9 @@ const RETRY_TIMES = Symbol.for('RETRY_TIMES')
69
69
  let skippableSuites = []
70
70
  let knownTests = {}
71
71
  let isCodeCoverageEnabled = false
72
+ let isCodeCoverageEnabledBecauseOfUs = false
72
73
  let isSuitesSkippingEnabled = false
74
+ let isKeepingCoverageConfiguration = false
73
75
  let isUserCodeCoverageEnabled = false
74
76
  let isSuitesSkipped = false
75
77
  let numSkippedSuites = 0
@@ -994,6 +996,8 @@ function getCliWrapper (isNewJestVersion) {
994
996
  if (!err) {
995
997
  isCodeCoverageEnabled = libraryConfig.isCodeCoverageEnabled
996
998
  isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
999
+ isKeepingCoverageConfiguration =
1000
+ libraryConfig.isKeepingCoverageConfiguration ?? isKeepingCoverageConfiguration
997
1001
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
998
1002
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
999
1003
  earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
@@ -1327,9 +1331,10 @@ function coverageReporterWrapper (coverageReporter) {
1327
1331
  */
1328
1332
  // `_addUntestedFiles` is an async function
1329
1333
  shimmer.wrap(CoverageReporter.prototype, '_addUntestedFiles', addUntestedFiles => function () {
1330
- // If the user has added coverage manually, they're willing to pay the price of this execution, so
1331
- // we will not skip it.
1332
- if (isSuitesSkippingEnabled && !isUserCodeCoverageEnabled) {
1334
+ if (isKeepingCoverageConfiguration) {
1335
+ return addUntestedFiles.apply(this, arguments)
1336
+ }
1337
+ if (isCodeCoverageEnabledBecauseOfUs) {
1333
1338
  return Promise.resolve()
1334
1339
  }
1335
1340
  return addUntestedFiles.apply(this, arguments)
@@ -1509,12 +1514,13 @@ function configureTestEnvironment (readConfigsResult) {
1509
1514
  }
1510
1515
 
1511
1516
  isUserCodeCoverageEnabled = !!readConfigsResult.globalConfig.collectCoverage
1517
+ isCodeCoverageEnabledBecauseOfUs = isCodeCoverageEnabled && !isUserCodeCoverageEnabled
1512
1518
 
1513
1519
  if (readConfigsResult.globalConfig.forceExit) {
1514
1520
  log.warn("Jest's '--forceExit' flag has been passed. This may cause loss of data.")
1515
1521
  }
1516
1522
 
1517
- if (isCodeCoverageEnabled) {
1523
+ if (isCodeCoverageEnabledBecauseOfUs) {
1518
1524
  const globalConfig = {
1519
1525
  ...readConfigsResult.globalConfig,
1520
1526
  collectCoverage: true,
@@ -1522,14 +1528,18 @@ function configureTestEnvironment (readConfigsResult) {
1522
1528
  readConfigsResult.globalConfig = globalConfig
1523
1529
  }
1524
1530
  if (isSuitesSkippingEnabled) {
1525
- // If suite skipping is enabled, the code coverage results are not going to be relevant,
1526
- // so we do not show them.
1527
- // Also, we might skip every test, so we need to pass `passWithNoTests`
1531
+ // If suite skipping is enabled, we pass `passWithNoTests` in case every test gets skipped.
1528
1532
  const globalConfig = {
1529
1533
  ...readConfigsResult.globalConfig,
1530
- coverageReporters: ['none'],
1531
1534
  passWithNoTests: true,
1532
1535
  }
1536
+ if (isCodeCoverageEnabledBecauseOfUs && !isKeepingCoverageConfiguration) {
1537
+ globalConfig.coverageReporters = ['none']
1538
+ readConfigsResult.configs = configs.map(config => ({
1539
+ ...config,
1540
+ coverageReporters: ['none'],
1541
+ }))
1542
+ }
1533
1543
  readConfigsResult.globalConfig = globalConfig
1534
1544
  }
1535
1545
 
@@ -1552,49 +1562,97 @@ function jestConfigSyncWrapper (jestConfig) {
1552
1562
  })
1553
1563
  }
1554
1564
 
1565
+ const DD_TEST_ENVIRONMENT_OPTION_KEYS = [
1566
+ '_ddTestModuleId',
1567
+ '_ddTestSessionId',
1568
+ '_ddTestCommand',
1569
+ '_ddTestSessionName',
1570
+ '_ddForcedToRun',
1571
+ '_ddUnskippable',
1572
+ '_ddItrCorrelationId',
1573
+ '_ddKnownTests',
1574
+ '_ddIsEarlyFlakeDetectionEnabled',
1575
+ '_ddEarlyFlakeDetectionSlowTestRetries',
1576
+ '_ddRepositoryRoot',
1577
+ '_ddIsFlakyTestRetriesEnabled',
1578
+ '_ddFlakyTestRetriesCount',
1579
+ '_ddIsDiEnabled',
1580
+ '_ddIsKnownTestsEnabled',
1581
+ '_ddIsTestManagementTestsEnabled',
1582
+ '_ddTestManagementTests',
1583
+ '_ddTestManagementAttemptToFixRetries',
1584
+ '_ddModifiedFiles',
1585
+ ]
1586
+
1587
+ function removeDatadogTestEnvironmentOptions (testEnvironmentOptions) {
1588
+ const removedEntries = []
1589
+
1590
+ for (const key of DD_TEST_ENVIRONMENT_OPTION_KEYS) {
1591
+ if (!Object.hasOwn(testEnvironmentOptions, key)) {
1592
+ continue
1593
+ }
1594
+
1595
+ removedEntries.push([key, testEnvironmentOptions[key]])
1596
+ delete testEnvironmentOptions[key]
1597
+ }
1598
+
1599
+ return function restoreDatadogTestEnvironmentOptions () {
1600
+ for (const [key, value] of removedEntries) {
1601
+ testEnvironmentOptions[key] = value
1602
+ }
1603
+ }
1604
+ }
1605
+
1606
+ /**
1607
+ * Wrap `createScriptTransformer` to temporarily hide Datadog-specific
1608
+ * `testEnvironmentOptions` keys while Jest builds its transform config.
1609
+ *
1610
+ * @param {Function} createScriptTransformer
1611
+ * @returns {Function}
1612
+ */
1613
+ function wrapCreateScriptTransformer (createScriptTransformer) {
1614
+ return function (config) {
1615
+ const testEnvironmentOptions = config?.testEnvironmentOptions
1616
+
1617
+ if (!testEnvironmentOptions) {
1618
+ return createScriptTransformer.apply(this, arguments)
1619
+ }
1620
+
1621
+ const restoreTestEnvironmentOptions = removeDatadogTestEnvironmentOptions(testEnvironmentOptions)
1622
+
1623
+ try {
1624
+ const result = createScriptTransformer.apply(this, arguments)
1625
+
1626
+ if (result?.then) {
1627
+ return result.finally(restoreTestEnvironmentOptions)
1628
+ }
1629
+
1630
+ restoreTestEnvironmentOptions()
1631
+ return result
1632
+ } catch (e) {
1633
+ restoreTestEnvironmentOptions()
1634
+ throw e
1635
+ }
1636
+ }
1637
+ }
1638
+
1555
1639
  addHook({
1556
1640
  name: '@jest/transform',
1557
- versions: ['>=24.8.0'],
1641
+ versions: ['>=24.8.0 <30.0.0'],
1558
1642
  file: 'build/ScriptTransformer.js',
1559
1643
  }, transformPackage => {
1560
- const originalCreateScriptTransformer = transformPackage.createScriptTransformer
1561
-
1562
- // `createScriptTransformer` is an async function
1563
- transformPackage.createScriptTransformer = function (config) {
1564
- const { testEnvironmentOptions, ...restOfConfig } = config
1565
- const {
1566
- _ddTestModuleId,
1567
- _ddTestSessionId,
1568
- _ddTestCommand,
1569
- _ddTestSessionName,
1570
- _ddForcedToRun,
1571
- _ddUnskippable,
1572
- _ddItrCorrelationId,
1573
- _ddKnownTests,
1574
- _ddIsEarlyFlakeDetectionEnabled,
1575
- _ddEarlyFlakeDetectionSlowTestRetries,
1576
- _ddRepositoryRoot,
1577
- _ddIsFlakyTestRetriesEnabled,
1578
- _ddFlakyTestRetriesCount,
1579
- _ddIsDiEnabled,
1580
- _ddIsKnownTestsEnabled,
1581
- _ddIsTestManagementTestsEnabled,
1582
- _ddTestManagementTests,
1583
- _ddTestManagementAttemptToFixRetries,
1584
- _ddModifiedFiles,
1585
- ...restOfTestEnvironmentOptions
1586
- } = testEnvironmentOptions
1587
-
1588
- restOfConfig.testEnvironmentOptions = restOfTestEnvironmentOptions
1589
-
1590
- arguments[0] = restOfConfig
1591
-
1592
- return originalCreateScriptTransformer.apply(this, arguments)
1593
- }
1644
+ transformPackage.createScriptTransformer = wrapCreateScriptTransformer(transformPackage.createScriptTransformer)
1594
1645
 
1595
1646
  return transformPackage
1596
1647
  })
1597
1648
 
1649
+ addHook({
1650
+ name: '@jest/transform',
1651
+ versions: ['>=30.0.0'],
1652
+ }, transformPackage => {
1653
+ return shimmer.wrap(transformPackage, 'createScriptTransformer', wrapCreateScriptTransformer, { replaceGetter: true })
1654
+ })
1655
+
1598
1656
  /**
1599
1657
  * Hook to remove the test paths (test suite) that are part of `skippableSuites`
1600
1658
  */
@@ -91,6 +91,11 @@ const {
91
91
  RUNTIME_VERSION,
92
92
  } = require('../../dd-trace/src/plugins/util/env')
93
93
  const { DD_MAJOR } = require('../../../version')
94
+ const {
95
+ resolveOriginalSourcePosition,
96
+ resolveSourceLineForTest,
97
+ shouldTrustInvocationDetailsLine,
98
+ } = require('./source-map-utils')
94
99
 
95
100
  const TEST_FRAMEWORK_NAME = 'cypress'
96
101
 
@@ -238,7 +243,6 @@ class CypressPlugin {
238
243
 
239
244
  finishedTestsByFile = {}
240
245
  testStatuses = {}
241
-
242
246
  isTestsSkipped = false
243
247
  isSuitesSkippingEnabled = false
244
248
  isCodeCoverageEnabled = false
@@ -391,7 +395,9 @@ class CypressPlugin {
391
395
  this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
392
396
 
393
397
  if (testSuiteAbsolutePath) {
394
- const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
398
+ const resolvedSuitePosition = resolveOriginalSourcePosition(testSuiteAbsolutePath, 1)
399
+ const resolvedSuiteAbsolutePath = resolvedSuitePosition ? resolvedSuitePosition.sourceFile : testSuiteAbsolutePath
400
+ const testSourceFile = getTestSuitePath(resolvedSuiteAbsolutePath, this.repositoryRoot)
395
401
  testSuiteSpanMetadata[TEST_SOURCE_FILE] = testSourceFile
396
402
  testSuiteSpanMetadata[TEST_SOURCE_START] = 1
397
403
  const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile })
@@ -804,8 +810,10 @@ class CypressPlugin {
804
810
  if (this.itrCorrelationId) {
805
811
  finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
806
812
  }
807
- const testSourceFile = spec.absolute && this.repositoryRoot
808
- ? getTestSuitePath(spec.absolute, this.repositoryRoot)
813
+ const resolvedSpecPosition = spec.absolute ? resolveOriginalSourcePosition(spec.absolute, 1) : null
814
+ const resolvedSpecAbsolutePath = resolvedSpecPosition ? resolvedSpecPosition.sourceFile : spec.absolute
815
+ const testSourceFile = resolvedSpecAbsolutePath && this.repositoryRoot
816
+ ? getTestSuitePath(resolvedSpecAbsolutePath, this.repositoryRoot)
809
817
  : spec.relative
810
818
  if (testSourceFile) {
811
819
  finishedTest.testSpan.setTag(TEST_SOURCE_FILE, testSourceFile)
@@ -902,9 +910,11 @@ class CypressPlugin {
902
910
  error,
903
911
  isRUMActive,
904
912
  testSourceLine,
913
+ testSourceStack,
905
914
  testSuite,
906
915
  testSuiteAbsolutePath,
907
916
  testName,
917
+ testItTitle,
908
918
  isNew,
909
919
  isEfdRetry,
910
920
  isAttemptToFix,
@@ -946,8 +956,29 @@ class CypressPlugin {
946
956
  if (isRUMActive) {
947
957
  this.activeTestSpan.setTag(TEST_IS_RUM_ACTIVE, 'true')
948
958
  }
959
+ // Source-line resolution strategy:
960
+ // 1. If plain JS and no source map, trust invocationDetails.line directly.
961
+ // 2. Otherwise, try invocationDetails.stack line mapped through source map.
962
+ // 3. If that fails, scan generated file for it/test/specify declaration by test name.
963
+ // 4. If declaration found:
964
+ // - .ts file: use declaration line directly.
965
+ // - .js file: map declaration line through source map.
966
+ // 5. If all fail, keep original invocationDetails.line.
949
967
  if (testSourceLine) {
950
- this.activeTestSpan.setTag(TEST_SOURCE_START, testSourceLine)
968
+ let resolvedLine = testSourceLine
969
+ if (testSuiteAbsolutePath && testItTitle) {
970
+ // Use invocationDetails directly only for plain JS specs without source maps.
971
+ // Otherwise, resolve from the test declaration in the spec and map via source map.
972
+ const shouldTrustInvocationDetails = shouldTrustInvocationDetailsLine(testSuiteAbsolutePath, testSourceLine)
973
+ if (!shouldTrustInvocationDetails) {
974
+ resolvedLine = resolveSourceLineForTest(
975
+ testSuiteAbsolutePath,
976
+ testItTitle,
977
+ testSourceStack
978
+ ) ?? testSourceLine
979
+ }
980
+ }
981
+ this.activeTestSpan.setTag(TEST_SOURCE_START, resolvedLine)
951
982
  }
952
983
  if (isNew) {
953
984
  this.activeTestSpan.setTag(TEST_IS_NEW, 'true')
@@ -0,0 +1,297 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ // Base64 lookup table for source map VLQ decoding
7
+ const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
8
+ const BASE64_DECODE = new Uint8Array(128)
9
+ for (let i = 0; i < BASE64_CHARS.length; i++) {
10
+ BASE64_DECODE[BASE64_CHARS.charCodeAt(i)] = i
11
+ }
12
+ const TEST_DECLARATION_RE = /(?:it|test|specify)\s*\(\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|`((?:[^`\\]|\\[\s\S])*)`)\s*,/g
13
+ const SOURCE_MAP_CACHE = new Map()
14
+
15
+ /**
16
+ * Whether a file path references JavaScript.
17
+ * @param {string} absoluteFilePath
18
+ * @returns {boolean}
19
+ */
20
+ function isJavaScriptFile (absoluteFilePath) {
21
+ return absoluteFilePath.endsWith('.js') || absoluteFilePath.endsWith('.cjs') || absoluteFilePath.endsWith('.mjs')
22
+ }
23
+
24
+ /**
25
+ * Decide whether invocationDetails line can be trusted as final source line.
26
+ * @param {string} absoluteFilePath
27
+ * @param {number} testSourceLine
28
+ * @returns {boolean}
29
+ */
30
+ function shouldTrustInvocationDetailsLine (absoluteFilePath, testSourceLine) {
31
+ if (!Number.isInteger(testSourceLine) || testSourceLine < 1) return false
32
+ if (!isJavaScriptFile(absoluteFilePath)) return false
33
+
34
+ return getCachedSourceMap(absoluteFilePath) === null
35
+ }
36
+
37
+ /**
38
+ * Decode one VLQ-encoded integer from `str` at `cursor.pos`, advancing the cursor in place.
39
+ * @param {string} str
40
+ * @param {{ pos: number }} cursor
41
+ * @returns {number}
42
+ */
43
+ function decodeVLQ (str, cursor) {
44
+ let result = 0
45
+ let shift = 0
46
+ let digit
47
+ do {
48
+ digit = BASE64_DECODE[str.charCodeAt(cursor.pos++)]
49
+ result |= (digit & 0x1F) << shift
50
+ shift += 5
51
+ } while (digit & 0x20)
52
+ return (result & 1) ? -(result >>> 1) : result >>> 1
53
+ }
54
+
55
+ /**
56
+ * Resolve a source path from a source map entry to an absolute file path.
57
+ * Handles regular relative paths and virtual URL-like source paths.
58
+ * @param {string} mapDir - Directory of the source map (or the file containing the inline source map)
59
+ * @param {string} sourceRoot - The `sourceRoot` field from the source map
60
+ * @param {string} sourcePath - A single entry from the source map's `sources` array
61
+ * @returns {string | null}
62
+ */
63
+ function resolveSourcePath (mapDir, sourceRoot, sourcePath) {
64
+ const cleanSourcePath = sourcePath.replace(/[?#].*$/, '')
65
+ if (/^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(cleanSourcePath)) {
66
+ // Virtual sources may use URL-like schemes (e.g. file://, webpack://, vite://).
67
+ // If they encode an absolute local path in the URL pathname, use it.
68
+ try {
69
+ const pathname = new URL(cleanSourcePath).pathname
70
+ return pathname && path.isAbsolute(pathname) ? pathname : null
71
+ } catch {
72
+ return null
73
+ }
74
+ }
75
+ if (sourceRoot && /^[A-Za-z][A-Za-z\d+.-]*:\/\//.test(sourceRoot)) {
76
+ // URL-like sourceRoot values are virtual; resolve relative entries from mapDir.
77
+ return path.resolve(mapDir, sourcePath)
78
+ }
79
+ return path.resolve(mapDir, sourceRoot || '', sourcePath)
80
+ }
81
+
82
+ /**
83
+ * Read a source map for a file. Tries:
84
+ * 1. An adjacent `.map` file (`absoluteFilePath + '.map'`)
85
+ * 2. An inline `data:` URI in the file's last line (`//# sourceMappingURL=data:…`)
86
+ * Returns null when neither source is available or parseable.
87
+ * @param {string} absoluteFilePath
88
+ * @returns {object | null}
89
+ */
90
+ function readSourceMap (absoluteFilePath) {
91
+ try {
92
+ return JSON.parse(fs.readFileSync(absoluteFilePath + '.map', 'utf8'))
93
+ } catch {}
94
+ try {
95
+ const content = fs.readFileSync(absoluteFilePath, 'utf8')
96
+ const match = content.match(
97
+ /\/\/# sourceMappingURL=data:application\/json;(?:charset=utf-8;)?base64,([\w+/=\s]+)/
98
+ )
99
+ if (match) {
100
+ return JSON.parse(Buffer.from(match[1].replaceAll(/\s/g, ''), 'base64').toString('utf8'))
101
+ }
102
+ } catch {}
103
+ return null
104
+ }
105
+
106
+ /**
107
+ * Read and cache source maps per file path. Cache stores parse result or null.
108
+ * @param {string} absoluteFilePath
109
+ * @returns {object | null}
110
+ */
111
+ function getCachedSourceMap (absoluteFilePath) {
112
+ if (SOURCE_MAP_CACHE.has(absoluteFilePath)) {
113
+ return SOURCE_MAP_CACHE.get(absoluteFilePath)
114
+ }
115
+ const sourceMap = readSourceMap(absoluteFilePath)
116
+ SOURCE_MAP_CACHE.set(absoluteFilePath, sourceMap)
117
+ return sourceMap
118
+ }
119
+
120
+ /**
121
+ * Given a generated file's absolute path and a generated line number, returns the
122
+ * original source file path and line by reading the adjacent .map file or an inline
123
+ * source map embedded in the file. Returns null when no source map is found or the
124
+ * mapping cannot be resolved.
125
+ * @param {string} absoluteFilePath - Absolute path to the generated (compiled or bundled) file
126
+ * @param {number} generatedLine - 1-indexed line number in the generated file
127
+ * @returns {{ sourceFile: string, line: number } | null}
128
+ */
129
+ function resolveOriginalSourcePosition (absoluteFilePath, generatedLine) {
130
+ const sourceMap = getCachedSourceMap(absoluteFilePath)
131
+ if (!sourceMap) return null
132
+ const { mappings, sources, sourceRoot } = sourceMap
133
+ if (!mappings || !sources?.length) return null
134
+
135
+ const mapDir = path.dirname(absoluteFilePath)
136
+ const cursor = { pos: 0 }
137
+ let srcFile = 0
138
+ let srcLine = 0
139
+
140
+ const lines = mappings.split(';')
141
+ for (let li = 0; li < lines.length; li++) {
142
+ const line = lines[li]
143
+ if (!line) continue
144
+ cursor.pos = 0
145
+ while (cursor.pos < line.length) {
146
+ decodeVLQ(line, cursor) // genCol — not needed
147
+ if (cursor.pos < line.length && line[cursor.pos] !== ',') {
148
+ // Segment has source info: srcFileIndex (delta), srcLine (delta), srcCol, [namesIndex]
149
+ srcFile += decodeVLQ(line, cursor)
150
+ srcLine += decodeVLQ(line, cursor)
151
+ decodeVLQ(line, cursor) // srcCol — not needed
152
+ if (cursor.pos < line.length && line[cursor.pos] !== ',') {
153
+ decodeVLQ(line, cursor) // namesIndex — not needed
154
+ }
155
+ if (li === generatedLine - 1) {
156
+ const sourcePath = sources[srcFile]
157
+ if (!sourcePath) return null
158
+ const sourceFile = resolveSourcePath(mapDir, sourceRoot, sourcePath)
159
+ return sourceFile ? { sourceFile, line: srcLine + 1 } : null
160
+ }
161
+ }
162
+ if (cursor.pos < line.length && line[cursor.pos] === ',') cursor.pos++
163
+ }
164
+ }
165
+ return null
166
+ }
167
+
168
+ /**
169
+ * Convert a template literal body (the text between backticks, with `${…}` interpolations)
170
+ * into a regex that matches the runtime-evaluated string. Each `${…}` expression is replaced
171
+ * with `.*?` so the pattern matches whatever value the expression produced at runtime.
172
+ * @param {string} templateBody - Raw template literal content (the text between the backticks)
173
+ * @returns {RegExp}
174
+ */
175
+ function templateBodyToRegExp (templateBody) {
176
+ // Split on ${...} expressions, escaping the literal parts and replacing interpolations
177
+ // with .*? wildcards. We handle basic nesting of braces inside ${} to avoid false splits.
178
+ let pattern = ''
179
+ let i = 0
180
+ while (i < templateBody.length) {
181
+ const dollarIdx = templateBody.indexOf('${', i)
182
+ if (dollarIdx === -1) {
183
+ pattern += templateBody.slice(i).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
184
+ break
185
+ }
186
+ pattern += templateBody.slice(i, dollarIdx).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)
187
+ pattern += '.*?'
188
+ // skip past the matching closing brace, counting nested braces
189
+ let depth = 1
190
+ i = dollarIdx + 2
191
+ while (i < templateBody.length && depth > 0) {
192
+ if (templateBody[i] === '{') depth++
193
+ else if (templateBody[i] === '}') depth--
194
+ i++
195
+ }
196
+ }
197
+ return new RegExp(`^${pattern}$`)
198
+ }
199
+
200
+ /**
201
+ * Count 1-indexed line number for a character index in `content`.
202
+ * @param {string} content
203
+ * @param {number} endIndex
204
+ * @returns {number}
205
+ */
206
+ function lineNumberForIndex (content, endIndex) {
207
+ let line = 1
208
+ for (let i = 0; i < endIndex; i++) {
209
+ if (content.charCodeAt(i) === 10) line++
210
+ }
211
+ return line
212
+ }
213
+
214
+ /**
215
+ * Extract the first stack frame line number from an invocation stack.
216
+ * Supports Chromium-style ("at fn (file:line:col)") and Firefox-style ("fn@file:line:col").
217
+ * @param {string} stack
218
+ * @returns {number | null}
219
+ */
220
+ function firstGeneratedLineFromStack (stack) {
221
+ if (typeof stack !== 'string' || stack.length === 0) return null
222
+ const lines = stack.split('\n')
223
+ for (const line of lines) {
224
+ const match = line.match(/:(\d+):\d+\)?\s*$/)
225
+ if (match) {
226
+ const parsed = Number(match[1])
227
+ if (Number.isInteger(parsed) && parsed > 0) {
228
+ return parsed
229
+ }
230
+ }
231
+ }
232
+ return null
233
+ }
234
+
235
+ /**
236
+ * Find the declaration line for a test name by scanning it()/test()/specify() calls.
237
+ * For template literals, `${...}` placeholders are fuzzy-matched against runtime values.
238
+ * @param {string} content
239
+ * @param {string} testName
240
+ * @returns {number | null}
241
+ */
242
+ function findTestDeclarationLine (content, testName) {
243
+ TEST_DECLARATION_RE.lastIndex = 0
244
+ let match
245
+ while ((match = TEST_DECLARATION_RE.exec(content)) !== null) {
246
+ const singleQuoted = match[1]
247
+ const doubleQuoted = match[2]
248
+ const templateQuoted = match[3]
249
+ const isTemplate = templateQuoted !== undefined
250
+ const candidateName = singleQuoted ?? doubleQuoted ?? templateQuoted
251
+ if (!candidateName) continue
252
+
253
+ const doesMatch = isTemplate
254
+ ? templateBodyToRegExp(candidateName).test(testName)
255
+ : candidateName === testName
256
+ if (doesMatch) {
257
+ return lineNumberForIndex(content, match.index)
258
+ }
259
+ }
260
+ return null
261
+ }
262
+
263
+ /**
264
+ * Find the original source line for a test.
265
+ * It first tries mapping a generated line extracted from invocation stack.
266
+ * If that fails, it scans declaration name and maps the matched generated line
267
+ * through a source map when available.
268
+ * For `.ts` specs, the matched line is already the source line.
269
+ * @param {string} absoluteFilePath - Absolute path to the spec file (compiled JS or .ts)
270
+ * @param {string} testName - The test name passed to `it()`, `test()`, or `specify()`
271
+ * @param {string} invocationStack - Raw invocationDetails stack for the test
272
+ * @returns {number | null} The resolved source line (1-indexed), or null
273
+ */
274
+ function resolveSourceLineForTest (absoluteFilePath, testName, invocationStack) {
275
+ const generatedLineFromStack = firstGeneratedLineFromStack(invocationStack)
276
+ if (generatedLineFromStack && !absoluteFilePath.endsWith('.ts')) {
277
+ const stackResolved = resolveOriginalSourcePosition(absoluteFilePath, generatedLineFromStack)
278
+ if (stackResolved) return stackResolved.line
279
+ }
280
+
281
+ let content
282
+ try {
283
+ content = fs.readFileSync(absoluteFilePath, 'utf8')
284
+ } catch {
285
+ return null
286
+ }
287
+
288
+ const foundLine = findTestDeclarationLine(content, testName)
289
+ if (!foundLine) return null
290
+
291
+ if (absoluteFilePath.endsWith('.ts')) return foundLine
292
+ const resolved = resolveOriginalSourcePosition(absoluteFilePath, foundLine)
293
+ if (resolved) return resolved.line
294
+ return null
295
+ }
296
+
297
+ module.exports = { resolveOriginalSourcePosition, resolveSourceLineForTest, shouldTrustInvocationDetailsLine }
@@ -258,6 +258,7 @@ afterEach(function () {
258
258
 
259
259
  const testInfo = {
260
260
  testName,
261
+ testItTitle: currentTest.title,
261
262
  testSuite: Cypress.mocha.getRootSuite().file,
262
263
  testSuiteAbsolutePath: Cypress.spec && Cypress.spec.absolute,
263
264
  // For quarantined tests, report the actual state (failed) to Datadog, not what Cypress thinks (passed)
@@ -272,7 +273,9 @@ afterEach(function () {
272
273
  isQuarantined: isQuarantinedTestThatFailed,
273
274
  }
274
275
  try {
275
- testInfo.testSourceLine = Cypress.mocha.getRunner().currentRunnable.invocationDetails.line
276
+ const invocationDetails = Cypress.mocha.getRunner().currentRunnable.invocationDetails
277
+ testInfo.testSourceLine = invocationDetails.line
278
+ testInfo.testSourceStack = invocationDetails.stack
276
279
  } catch {}
277
280
 
278
281
  const rum = safeGetRum(originalWindow)
@@ -175,7 +175,7 @@ class AIGuard extends NoopAIGuard {
175
175
  `AI Guard service call failed, status ${response.status}`,
176
176
  { errors: response.body?.errors })
177
177
  }
178
- let action, reason, tags, blockingEnabled
178
+ let action, reason, tags, sdsFindings, blockingEnabled
179
179
  try {
180
180
  const attr = response.body.data.attributes
181
181
  if (!attr.action) {
@@ -184,6 +184,7 @@ class AIGuard extends NoopAIGuard {
184
184
  action = attr.action
185
185
  reason = attr.reason
186
186
  tags = attr.tags
187
+ sdsFindings = attr.sds_findings
187
188
  blockingEnabled = attr.is_blocking_enabled ?? false
188
189
  } catch (e) {
189
190
  appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1)
@@ -198,6 +199,9 @@ class AIGuard extends NoopAIGuard {
198
199
  if (tags?.length > 0) {
199
200
  metaStruct.attack_categories = tags
200
201
  }
202
+ if (sdsFindings?.length > 0) {
203
+ metaStruct.sds = sdsFindings
204
+ }
201
205
  if (shouldBlock) {
202
206
  span.setTag(AI_GUARD_BLOCKED_TAG_KEY, 'true')
203
207
  throw new AIGuardAbortError(reason, tags)
@@ -234,6 +234,7 @@ class CiVisibilityExporter extends BufferingExporter {
234
234
  testManagementAttemptToFixRetries ?? this._config.testManagementAttemptToFixRetries,
235
235
  isImpactedTestsEnabled: isImpactedTestsEnabled && this._config.isImpactedTestsEnabled,
236
236
  isCoverageReportUploadEnabled,
237
+ isKeepingCoverageConfiguration: this._config.isKeepingCoverageConfiguration,
237
238
  }
238
239
  }
239
240
 
@@ -352,6 +352,7 @@ class Config {
352
352
  DD_TELEMETRY_HEARTBEAT_INTERVAL,
353
353
  DD_TELEMETRY_LOG_COLLECTION_ENABLED,
354
354
  DD_TELEMETRY_METRICS_ENABLED,
355
+ DD_TEST_TIA_KEEP_COV_CONFIG,
355
356
  DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED,
356
357
  DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED,
357
358
  DD_TRACE_AGENT_PORT,
@@ -757,6 +758,7 @@ class Config {
757
758
  unprocessedTarget['telemetry.heartbeatInterval'] = DD_TELEMETRY_HEARTBEAT_INTERVAL
758
759
  setBoolean(target, 'telemetry.logCollection', DD_TELEMETRY_LOG_COLLECTION_ENABLED)
759
760
  setBoolean(target, 'telemetry.metrics', DD_TELEMETRY_METRICS_ENABLED)
761
+ setBoolean(target, 'isKeepingCoverageConfiguration', DD_TEST_TIA_KEEP_COV_CONFIG)
760
762
  setBoolean(target, 'traceId128BitGenerationEnabled', DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED)
761
763
  setBoolean(target, 'traceId128BitLoggingEnabled', DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED)
762
764
  warnIfPropagationStyleConflict(
@@ -1770,6 +1770,16 @@
1770
1770
  ]
1771
1771
  }
1772
1772
  ],
1773
+ "DD_TEST_TIA_KEEP_COV_CONFIG": [
1774
+ {
1775
+ "implementation": "A",
1776
+ "type": "boolean",
1777
+ "default": "false",
1778
+ "configurationNames": [
1779
+ "isKeepingCoverageConfiguration"
1780
+ ]
1781
+ }
1782
+ ],
1773
1783
  "DD_TEST_SESSION_NAME": [
1774
1784
  {
1775
1785
  "implementation": "A",