dd-trace 5.87.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/LICENSE-3rdparty.csv +60 -32
- package/ext/exporters.d.ts +1 -0
- package/ext/exporters.js +1 -0
- package/ext/tags.js +2 -0
- package/index.d.ts +234 -4
- package/package.json +18 -11
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +27 -110
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/compiler.js +74 -0
- 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} +143 -17
- package/packages/datadog-instrumentations/src/jest.js +176 -54
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
- package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
- package/packages/datadog-plugin-cucumber/src/index.js +9 -6
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +62 -5
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
- package/packages/datadog-plugin-cypress/src/support.js +52 -9
- package/packages/datadog-plugin-jest/src/index.js +12 -2
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
- package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
- package/packages/datadog-plugin-mocha/src/index.js +9 -6
- package/packages/datadog-plugin-playwright/src/index.js +10 -6
- package/packages/datadog-plugin-vitest/src/index.js +13 -8
- package/packages/dd-trace/src/aiguard/sdk.js +5 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
- package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
- package/packages/dd-trace/src/azure_metadata.js +0 -2
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +3 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +148 -197
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +38 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4125 -512
- package/packages/dd-trace/src/constants.js +0 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
- 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/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +140 -4
- package/packages/dd-trace/src/encode/agentless-json.js +155 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +21 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
- package/packages/dd-trace/src/exporters/common/request.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -4
- package/packages/dd-trace/src/pkg.js +1 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
- package/packages/dd-trace/src/plugins/database.js +15 -2
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
- package/packages/dd-trace/src/propagation-hash/index.js +145 -0
- package/packages/dd-trace/src/proxy.js +6 -1
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +53 -19
- 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/compiler.js +0 -33
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
- package/packages/dd-trace/src/scope/noop/scope.js +0 -21
|
@@ -10,6 +10,12 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
10
10
|
ctx.currentStore?.span?.finish()
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
start (ctx) {
|
|
14
|
+
if (!this.config.dsmEnabled) return
|
|
15
|
+
const { span } = ctx.currentStore
|
|
16
|
+
this.setProducerCheckpoint(span, ctx)
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
bindStart (ctx) {
|
|
14
20
|
const { resource, meta } = this.getSpanData(ctx)
|
|
15
21
|
const span = this.startSpan({
|
|
@@ -25,10 +31,6 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
25
31
|
|
|
26
32
|
this.injectTraceContext(span, ctx)
|
|
27
33
|
|
|
28
|
-
if (this.config.dsmEnabled) {
|
|
29
|
-
this.setProducerCheckpoint(span, ctx)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
34
|
return ctx.currentStore
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -40,13 +42,24 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
40
42
|
throw new Error('injectTraceContext must be implemented by subclass')
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
_injectIntoOpts (span, opts) {
|
|
46
|
+
const carrier = {}
|
|
47
|
+
this.tracer.inject(span, 'text_map', carrier)
|
|
48
|
+
const existing = opts.telemetry?.metadata ? JSON.parse(opts.telemetry.metadata) : {}
|
|
49
|
+
existing._datadog = carrier
|
|
50
|
+
opts.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
setProducerCheckpoint (span, ctx) {
|
|
44
|
-
const { queueName, payloadSize,
|
|
54
|
+
const { queueName, payloadSize, optsTarget } = this.getDsmData(ctx)
|
|
45
55
|
const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
|
|
46
56
|
const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
|
|
58
|
+
if (optsTarget && typeof optsTarget === 'object') {
|
|
59
|
+
const existing = optsTarget.telemetry?.metadata ? JSON.parse(optsTarget.telemetry.metadata) : {}
|
|
60
|
+
DsmPathwayCodec.encode(dataStreamsContext, existing._datadog || existing)
|
|
61
|
+
if (!existing._datadog) existing._datadog = {}
|
|
62
|
+
optsTarget.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
|
@@ -68,12 +81,22 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
|
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
#ensureOpts (ctx) {
|
|
85
|
+
let opts = ctx.arguments?.[2]
|
|
86
|
+
if (!opts || typeof opts !== 'object') {
|
|
87
|
+
opts = {}
|
|
88
|
+
if (ctx.arguments.length <= 2) {
|
|
89
|
+
Array.prototype.push.call(ctx.arguments, opts)
|
|
90
|
+
} else {
|
|
91
|
+
ctx.arguments[2] = opts
|
|
92
|
+
}
|
|
76
93
|
}
|
|
94
|
+
return opts
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
injectTraceContext (span, ctx) {
|
|
98
|
+
const opts = this.#ensureOpts(ctx)
|
|
99
|
+
this._injectIntoOpts(span, opts)
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
getDsmData (ctx) {
|
|
@@ -81,7 +104,7 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
|
|
|
81
104
|
return {
|
|
82
105
|
queueName: ctx.self?.name || 'bullmq',
|
|
83
106
|
payloadSize: data ? getMessageSize(data) : 0,
|
|
84
|
-
|
|
107
|
+
optsTarget: this.#ensureOpts(ctx),
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
}
|
|
@@ -108,10 +131,11 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
108
131
|
injectTraceContext (span, ctx) {
|
|
109
132
|
const jobs = ctx.arguments?.[0]
|
|
110
133
|
if (!Array.isArray(jobs)) return
|
|
134
|
+
|
|
111
135
|
for (const job of jobs) {
|
|
112
|
-
if (job
|
|
113
|
-
job.
|
|
114
|
-
this.
|
|
136
|
+
if (!job) continue
|
|
137
|
+
job.opts = job.opts || {}
|
|
138
|
+
this._injectIntoOpts(span, job.opts)
|
|
115
139
|
}
|
|
116
140
|
}
|
|
117
141
|
|
|
@@ -123,7 +147,7 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
123
147
|
return {
|
|
124
148
|
queueName: ctx.self?.name || 'bullmq',
|
|
125
149
|
payloadSize,
|
|
126
|
-
|
|
150
|
+
optsTarget: jobs[0]?.opts,
|
|
127
151
|
}
|
|
128
152
|
}
|
|
129
153
|
|
|
@@ -133,12 +157,14 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
133
157
|
const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
|
|
134
158
|
|
|
135
159
|
for (const job of jobs) {
|
|
136
|
-
if (job?.data
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
160
|
+
if (!job?.data) continue
|
|
161
|
+
const payloadSize = getMessageSize(job.data)
|
|
162
|
+
const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
163
|
+
job.opts = job.opts || {}
|
|
164
|
+
const existing = job.opts.telemetry?.metadata ? JSON.parse(job.opts.telemetry.metadata) : {}
|
|
165
|
+
DsmPathwayCodec.encode(dataStreamsContext, existing._datadog || existing)
|
|
166
|
+
if (!existing._datadog) existing._datadog = {}
|
|
167
|
+
job.opts.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
}
|
|
@@ -159,18 +185,21 @@ class FlowProducerAddPlugin extends BaseBullmqProducerPlugin {
|
|
|
159
185
|
|
|
160
186
|
injectTraceContext (span, ctx) {
|
|
161
187
|
const flow = ctx.arguments?.[0]
|
|
162
|
-
if (flow
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
188
|
+
if (!flow) return
|
|
189
|
+
flow.opts = flow.opts || {}
|
|
190
|
+
this._injectIntoOpts(span, flow.opts)
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
getDsmData (ctx) {
|
|
169
194
|
const flow = ctx.arguments?.[0]
|
|
195
|
+
if (!flow) {
|
|
196
|
+
return { queueName: 'bullmq', payloadSize: 0, optsTarget: undefined }
|
|
197
|
+
}
|
|
198
|
+
flow.opts = flow.opts || {}
|
|
170
199
|
return {
|
|
171
|
-
queueName: flow
|
|
172
|
-
payloadSize: flow
|
|
173
|
-
|
|
200
|
+
queueName: flow.queueName || 'bullmq',
|
|
201
|
+
payloadSize: flow.data ? getMessageSize(flow.data) : 0,
|
|
202
|
+
optsTarget: flow.opts,
|
|
174
203
|
}
|
|
175
204
|
}
|
|
176
205
|
}
|
|
@@ -128,12 +128,15 @@ class CucumberPlugin extends CiPlugin {
|
|
|
128
128
|
const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd())
|
|
129
129
|
const testSourceFile = getTestSuitePath(testFileAbsolutePath, this.repositoryRoot)
|
|
130
130
|
|
|
131
|
-
const testSuiteMetadata =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
const testSuiteMetadata = {
|
|
132
|
+
...getTestSuiteCommonTags(
|
|
133
|
+
this.command,
|
|
134
|
+
this.frameworkVersion,
|
|
135
|
+
testSuitePath,
|
|
136
|
+
'cucumber'
|
|
137
|
+
),
|
|
138
|
+
...this.getSessionRequestErrorTags(),
|
|
139
|
+
}
|
|
137
140
|
if (isUnskippable) {
|
|
138
141
|
this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
|
|
139
142
|
testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
|
|
@@ -46,6 +46,8 @@ const {
|
|
|
46
46
|
TEST_RETRY_REASON_TYPES,
|
|
47
47
|
getPullRequestDiff,
|
|
48
48
|
getModifiedFilesFromDiff,
|
|
49
|
+
getSessionRequestErrorTags,
|
|
50
|
+
DD_CI_LIBRARY_CONFIGURATION_ERROR,
|
|
49
51
|
TEST_IS_MODIFIED,
|
|
50
52
|
getPullRequestBaseBranch,
|
|
51
53
|
} = require('../../dd-trace/src/plugins/util/test')
|
|
@@ -89,6 +91,11 @@ const {
|
|
|
89
91
|
RUNTIME_VERSION,
|
|
90
92
|
} = require('../../dd-trace/src/plugins/util/env')
|
|
91
93
|
const { DD_MAJOR } = require('../../../version')
|
|
94
|
+
const {
|
|
95
|
+
resolveOriginalSourcePosition,
|
|
96
|
+
resolveSourceLineForTest,
|
|
97
|
+
shouldTrustInvocationDetailsLine,
|
|
98
|
+
} = require('./source-map-utils')
|
|
92
99
|
|
|
93
100
|
const TEST_FRAMEWORK_NAME = 'cypress'
|
|
94
101
|
|
|
@@ -236,7 +243,6 @@ class CypressPlugin {
|
|
|
236
243
|
|
|
237
244
|
finishedTestsByFile = {}
|
|
238
245
|
testStatuses = {}
|
|
239
|
-
|
|
240
246
|
isTestsSkipped = false
|
|
241
247
|
isSuitesSkippingEnabled = false
|
|
242
248
|
isCodeCoverageEnabled = false
|
|
@@ -306,6 +312,9 @@ class CypressPlugin {
|
|
|
306
312
|
|
|
307
313
|
this.isTestIsolationEnabled = getIsTestIsolationEnabled(cypressConfig)
|
|
308
314
|
|
|
315
|
+
const envFlushWait = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS'))
|
|
316
|
+
this.rumFlushWaitMillis = Number.isFinite(envFlushWait) ? envFlushWait : undefined
|
|
317
|
+
|
|
309
318
|
if (!this.isTestIsolationEnabled) {
|
|
310
319
|
log.warn('Test isolation is disabled, retries will not be enabled')
|
|
311
320
|
}
|
|
@@ -314,10 +323,15 @@ class CypressPlugin {
|
|
|
314
323
|
this.testEnvironmentMetadata[DD_TEST_IS_USER_PROVIDED_SERVICE] =
|
|
315
324
|
tracer._tracer._config.isServiceUserProvided ? 'true' : 'false'
|
|
316
325
|
|
|
326
|
+
this._pendingRequestErrorTags = []
|
|
317
327
|
this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration)
|
|
318
328
|
.then((libraryConfigurationResponse) => {
|
|
319
329
|
if (libraryConfigurationResponse.err) {
|
|
320
330
|
log.error('Cypress plugin library config response error', libraryConfigurationResponse.err)
|
|
331
|
+
this._pendingRequestErrorTags.push({
|
|
332
|
+
tag: DD_CI_LIBRARY_CONFIGURATION_ERROR,
|
|
333
|
+
value: 'true',
|
|
334
|
+
})
|
|
321
335
|
} else {
|
|
322
336
|
const {
|
|
323
337
|
libraryConfig: {
|
|
@@ -381,7 +395,9 @@ class CypressPlugin {
|
|
|
381
395
|
this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
|
|
382
396
|
|
|
383
397
|
if (testSuiteAbsolutePath) {
|
|
384
|
-
const
|
|
398
|
+
const resolvedSuitePosition = resolveOriginalSourcePosition(testSuiteAbsolutePath, 1)
|
|
399
|
+
const resolvedSuiteAbsolutePath = resolvedSuitePosition ? resolvedSuitePosition.sourceFile : testSuiteAbsolutePath
|
|
400
|
+
const testSourceFile = getTestSuitePath(resolvedSuiteAbsolutePath, this.repositoryRoot)
|
|
385
401
|
testSuiteSpanMetadata[TEST_SOURCE_FILE] = testSourceFile
|
|
386
402
|
testSuiteSpanMetadata[TEST_SOURCE_START] = 1
|
|
387
403
|
const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile })
|
|
@@ -412,6 +428,7 @@ class CypressPlugin {
|
|
|
412
428
|
if (this.testSessionSpan && this.testModuleSpan) {
|
|
413
429
|
testSuiteTags[TEST_SESSION_ID] = this.testSessionSpan.context().toTraceId()
|
|
414
430
|
testSuiteTags[TEST_MODULE_ID] = this.testModuleSpan.context().toSpanId()
|
|
431
|
+
Object.assign(testSuiteTags, this.getSessionRequestErrorTags())
|
|
415
432
|
// If testSuiteSpan couldn't be created, we'll use the testModuleSpan as the parent
|
|
416
433
|
if (!this.testSuiteSpan) {
|
|
417
434
|
testSuiteTags[TEST_SUITE_ID] = this.testModuleSpan.context().toSpanId()
|
|
@@ -468,6 +485,14 @@ class CypressPlugin {
|
|
|
468
485
|
})
|
|
469
486
|
}
|
|
470
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Returns request error tags from the test session span for propagation to test spans.
|
|
490
|
+
* @returns {Record<string, string>}
|
|
491
|
+
*/
|
|
492
|
+
getSessionRequestErrorTags () {
|
|
493
|
+
return getSessionRequestErrorTags(this.testSessionSpan)
|
|
494
|
+
}
|
|
495
|
+
|
|
471
496
|
ciVisEvent (name, testLevel, tags = {}) {
|
|
472
497
|
incrementCountMetric(name, {
|
|
473
498
|
testLevel,
|
|
@@ -596,14 +621,20 @@ class CypressPlugin {
|
|
|
596
621
|
},
|
|
597
622
|
integrationName: TEST_FRAMEWORK_NAME,
|
|
598
623
|
})
|
|
624
|
+
for (const { tag, value } of this._pendingRequestErrorTags) {
|
|
625
|
+
this.testSessionSpan.setTag(tag, value)
|
|
626
|
+
}
|
|
627
|
+
this._pendingRequestErrorTags = []
|
|
599
628
|
this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session')
|
|
600
629
|
|
|
630
|
+
const sessionRequestErrorTags = getSessionRequestErrorTags(this.testSessionSpan)
|
|
601
631
|
this.testModuleSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_module`, {
|
|
602
632
|
childOf: this.testSessionSpan,
|
|
603
633
|
tags: {
|
|
604
634
|
[COMPONENT]: TEST_FRAMEWORK_NAME,
|
|
605
635
|
...this.testEnvironmentMetadata,
|
|
606
636
|
...testModuleSpanMetadata,
|
|
637
|
+
...sessionRequestErrorTags,
|
|
607
638
|
},
|
|
608
639
|
integrationName: TEST_FRAMEWORK_NAME,
|
|
609
640
|
})
|
|
@@ -779,8 +810,10 @@ class CypressPlugin {
|
|
|
779
810
|
if (this.itrCorrelationId) {
|
|
780
811
|
finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
|
|
781
812
|
}
|
|
782
|
-
const
|
|
783
|
-
|
|
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)
|
|
784
817
|
: spec.relative
|
|
785
818
|
if (testSourceFile) {
|
|
786
819
|
finishedTest.testSpan.setTag(TEST_SOURCE_FILE, testSourceFile)
|
|
@@ -823,6 +856,7 @@ class CypressPlugin {
|
|
|
823
856
|
isModifiedTest: this.getIsTestModified(testSuiteAbsolutePath),
|
|
824
857
|
repositoryRoot: this.repositoryRoot,
|
|
825
858
|
isTestIsolationEnabled: this.isTestIsolationEnabled,
|
|
859
|
+
rumFlushWaitMillis: this.rumFlushWaitMillis,
|
|
826
860
|
}
|
|
827
861
|
|
|
828
862
|
if (this.testSuiteSpan) {
|
|
@@ -876,9 +910,11 @@ class CypressPlugin {
|
|
|
876
910
|
error,
|
|
877
911
|
isRUMActive,
|
|
878
912
|
testSourceLine,
|
|
913
|
+
testSourceStack,
|
|
879
914
|
testSuite,
|
|
880
915
|
testSuiteAbsolutePath,
|
|
881
916
|
testName,
|
|
917
|
+
testItTitle,
|
|
882
918
|
isNew,
|
|
883
919
|
isEfdRetry,
|
|
884
920
|
isAttemptToFix,
|
|
@@ -920,8 +956,29 @@ class CypressPlugin {
|
|
|
920
956
|
if (isRUMActive) {
|
|
921
957
|
this.activeTestSpan.setTag(TEST_IS_RUM_ACTIVE, 'true')
|
|
922
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.
|
|
923
967
|
if (testSourceLine) {
|
|
924
|
-
|
|
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)
|
|
925
982
|
}
|
|
926
983
|
if (isNew) {
|
|
927
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 }
|