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.
- package/ext/tags.js +2 -0
- package/index.d.ts +9 -0
- package/package.json +12 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +26 -111
- package/packages/datadog-instrumentations/src/helpers/rewriter/{compiler.js → orchestrion/compiler.js} +5 -5
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +43 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +49 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +121 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/{transforms.js → orchestrion/transforms.js} +6 -6
- package/packages/datadog-instrumentations/src/jest.js +101 -43
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +36 -5
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
- package/packages/datadog-plugin-cypress/src/support.js +4 -1
- package/packages/dd-trace/src/aiguard/sdk.js +5 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +1 -0
- package/packages/dd-trace/src/config/index.js +2 -0
- package/packages/dd-trace/src/config/supported-configurations.json +10 -0
- package/packages/dd-trace/src/datastreams/checkpointer.js +13 -0
- package/packages/dd-trace/src/datastreams/index.js +3 -0
- package/packages/dd-trace/src/datastreams/manager.js +9 -0
- package/packages/dd-trace/src/datastreams/processor.js +126 -3
- package/packages/dd-trace/src/encode/agentless-json.js +16 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +7 -8
- package/packages/dd-trace/src/pkg.js +1 -1
- package/packages/dd-trace/src/proxy.js +2 -1
- package/packages/dd-trace/src/startup-log.js +52 -18
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/@datadog/source-map/index.js +1 -1
- package/vendor/dist/@isaacs/ttlcache/index.js +1 -1
- package/vendor/dist/@opentelemetry/core/index.js +1 -1
- package/vendor/dist/@opentelemetry/resources/index.js +1 -1
- package/vendor/dist/astring/index.js +1 -1
- package/vendor/dist/crypto-randomuuid/index.js +1 -1
- package/vendor/dist/escape-string-regexp/index.js +1 -1
- package/vendor/dist/esquery/index.js +1 -1
- package/vendor/dist/ignore/index.js +1 -1
- package/vendor/dist/istanbul-lib-coverage/index.js +1 -1
- package/vendor/dist/jest-docblock/index.js +1 -1
- package/vendor/dist/jsonpath-plus/index.js +1 -1
- package/vendor/dist/limiter/index.js +1 -1
- package/vendor/dist/lodash.sortby/index.js +1 -1
- package/vendor/dist/lru-cache/index.js +1 -1
- package/vendor/dist/meriyah/index.js +1 -1
- package/vendor/dist/module-details-from-path/index.js +1 -1
- package/vendor/dist/mutexify/promise/index.js +1 -1
- package/vendor/dist/opentracing/index.js +1 -1
- package/vendor/dist/path-to-regexp/index.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/retry/index.js +1 -1
- package/vendor/dist/rfdc/index.js +1 -1
- package/vendor/dist/semifies/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/vendor/dist/source-map/index.js +1 -1
- package/vendor/dist/source-map/lib/util/index.js +1 -1
- package/vendor/dist/tlhunter-sorted-set/index.js +1 -1
- package/vendor/dist/ttl-set/index.js +1 -1
- 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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
808
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|