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.
Files changed (119) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/ext/tags.js +2 -0
  5. package/index.d.ts +234 -4
  6. package/package.json +18 -11
  7. package/packages/datadog-instrumentations/src/ai.js +54 -90
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +27 -110
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/compiler.js +74 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +43 -0
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +49 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +121 -0
  17. package/packages/datadog-instrumentations/src/helpers/rewriter/{transforms.js → orchestrion/transforms.js} +143 -17
  18. package/packages/datadog-instrumentations/src/jest.js +176 -54
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  21. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  22. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  23. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  24. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  25. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  26. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +62 -5
  27. package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
  28. package/packages/datadog-plugin-cypress/src/support.js +52 -9
  29. package/packages/datadog-plugin-jest/src/index.js +12 -2
  30. package/packages/datadog-plugin-jest/src/util.js +2 -1
  31. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  32. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  33. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  34. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  35. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  36. package/packages/dd-trace/src/aiguard/sdk.js +5 -1
  37. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  39. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  40. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  41. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  42. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  45. package/packages/dd-trace/src/azure_metadata.js +0 -2
  46. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  47. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +3 -0
  48. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  49. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  50. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  51. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  52. package/packages/dd-trace/src/config/defaults.js +148 -197
  53. package/packages/dd-trace/src/config/helper.js +43 -1
  54. package/packages/dd-trace/src/config/index.js +38 -14
  55. package/packages/dd-trace/src/config/supported-configurations.json +4125 -512
  56. package/packages/dd-trace/src/constants.js +0 -2
  57. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  58. package/packages/dd-trace/src/datastreams/checkpointer.js +13 -0
  59. package/packages/dd-trace/src/datastreams/index.js +3 -0
  60. package/packages/dd-trace/src/datastreams/manager.js +9 -0
  61. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  62. package/packages/dd-trace/src/datastreams/processor.js +140 -4
  63. package/packages/dd-trace/src/encode/agentless-json.js +155 -0
  64. package/packages/dd-trace/src/exporter.js +2 -0
  65. package/packages/dd-trace/src/exporters/agent/writer.js +21 -8
  66. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  67. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  68. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  69. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  70. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  71. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  72. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  73. package/packages/dd-trace/src/opentracing/span.js +6 -4
  74. package/packages/dd-trace/src/pkg.js +1 -1
  75. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  76. package/packages/dd-trace/src/plugins/database.js +15 -2
  77. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  78. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  79. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  80. package/packages/dd-trace/src/proxy.js +6 -1
  81. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  82. package/packages/dd-trace/src/startup-log.js +53 -19
  83. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  84. package/vendor/dist/@datadog/source-map/index.js +1 -1
  85. package/vendor/dist/@isaacs/ttlcache/index.js +1 -1
  86. package/vendor/dist/@opentelemetry/core/index.js +1 -1
  87. package/vendor/dist/@opentelemetry/resources/index.js +1 -1
  88. package/vendor/dist/astring/index.js +1 -1
  89. package/vendor/dist/crypto-randomuuid/index.js +1 -1
  90. package/vendor/dist/escape-string-regexp/index.js +1 -1
  91. package/vendor/dist/esquery/index.js +1 -1
  92. package/vendor/dist/ignore/index.js +1 -1
  93. package/vendor/dist/istanbul-lib-coverage/index.js +1 -1
  94. package/vendor/dist/jest-docblock/index.js +1 -1
  95. package/vendor/dist/jsonpath-plus/index.js +1 -1
  96. package/vendor/dist/limiter/index.js +1 -1
  97. package/vendor/dist/lodash.sortby/index.js +1 -1
  98. package/vendor/dist/lru-cache/index.js +1 -1
  99. package/vendor/dist/meriyah/index.js +1 -1
  100. package/vendor/dist/module-details-from-path/index.js +1 -1
  101. package/vendor/dist/mutexify/promise/index.js +1 -1
  102. package/vendor/dist/opentracing/index.js +1 -1
  103. package/vendor/dist/path-to-regexp/index.js +1 -1
  104. package/vendor/dist/pprof-format/index.js +1 -1
  105. package/vendor/dist/protobufjs/index.js +1 -1
  106. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  107. package/vendor/dist/retry/index.js +1 -1
  108. package/vendor/dist/rfdc/index.js +1 -1
  109. package/vendor/dist/semifies/index.js +1 -1
  110. package/vendor/dist/shell-quote/index.js +1 -1
  111. package/vendor/dist/source-map/index.js +1 -1
  112. package/vendor/dist/source-map/lib/util/index.js +1 -1
  113. package/vendor/dist/tlhunter-sorted-set/index.js +1 -1
  114. package/vendor/dist/ttl-set/index.js +1 -1
  115. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +0 -33
  116. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  117. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  118. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  119. 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, injectTarget } = this.getDsmData(ctx)
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
- if (injectTarget && typeof injectTarget === 'object') {
48
- injectTarget._datadog = injectTarget._datadog || {}
49
- DsmPathwayCodec.encode(dataStreamsContext, injectTarget._datadog)
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
- injectTraceContext (span, ctx) {
72
- const data = ctx.arguments?.[1]
73
- if (data?.constructor?.name === 'Object') {
74
- data._datadog = data._datadog || {}
75
- this.tracer.inject(span, 'text_map', data._datadog)
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
- injectTarget: data,
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?.data?.constructor?.name !== 'Object') continue
113
- job.data._datadog = job.data._datadog || {}
114
- this.tracer.inject(span, 'text_map', job.data._datadog)
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
- injectTarget: jobs[0]?.data,
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 && job.data !== null && job.data.constructor.name === 'Object') {
137
- const payloadSize = getMessageSize(job.data)
138
- const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
139
- job.data._datadog = job.data._datadog || {}
140
- DsmPathwayCodec.encode(dataStreamsContext, job.data._datadog)
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?.data?.constructor?.name === 'Object') {
163
- flow.data._datadog = flow.data._datadog || {}
164
- this.tracer.inject(span, 'text_map', flow.data._datadog)
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?.queueName || 'bullmq',
172
- payloadSize: flow?.data ? getMessageSize(flow.data) : 0,
173
- injectTarget: flow?.data,
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 = getTestSuiteCommonTags(
132
- this.command,
133
- this.frameworkVersion,
134
- testSuitePath,
135
- 'cucumber'
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 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)
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 testSourceFile = spec.absolute && this.repositoryRoot
783
- ? 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)
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
- 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)
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 }