dd-trace 5.85.0 → 5.86.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 (39) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +1 -1
  3. package/packages/datadog-core/src/storage.js +30 -12
  4. package/packages/datadog-instrumentations/src/jest.js +34 -9
  5. package/packages/datadog-instrumentations/src/mocha/main.js +9 -0
  6. package/packages/datadog-instrumentations/src/prisma.js +225 -30
  7. package/packages/datadog-instrumentations/src/ws.js +22 -0
  8. package/packages/datadog-plugin-cucumber/src/index.js +4 -10
  9. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +5 -1
  10. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +2 -4
  11. package/packages/datadog-plugin-http/src/server.js +23 -8
  12. package/packages/datadog-plugin-jest/src/index.js +29 -10
  13. package/packages/datadog-plugin-jest/src/util.js +7 -1
  14. package/packages/datadog-plugin-mocha/src/index.js +5 -17
  15. package/packages/datadog-plugin-playwright/src/index.js +3 -0
  16. package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +37 -14
  17. package/packages/datadog-plugin-prisma/src/index.js +8 -5
  18. package/packages/datadog-plugin-router/src/index.js +28 -19
  19. package/packages/datadog-plugin-vitest/src/index.js +6 -10
  20. package/packages/datadog-plugin-ws/src/server.js +8 -0
  21. package/packages/dd-trace/src/appsec/iast/path-line.js +1 -0
  22. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  23. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +19 -0
  24. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +15 -0
  25. package/packages/dd-trace/src/ci-visibility/telemetry.js +36 -0
  26. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +44 -1
  27. package/packages/dd-trace/src/exporters/common/request.js +35 -35
  28. package/packages/dd-trace/src/id.js +1 -1
  29. package/packages/dd-trace/src/lambda/context.js +27 -0
  30. package/packages/dd-trace/src/lambda/handler.js +5 -18
  31. package/packages/dd-trace/src/log/writer.js +1 -5
  32. package/packages/dd-trace/src/plugins/ci_plugin.js +63 -1
  33. package/packages/dd-trace/src/plugins/util/git.js +27 -30
  34. package/packages/dd-trace/src/plugins/util/test.js +3 -1
  35. package/packages/dd-trace/src/plugins/util/web.js +1 -0
  36. package/packages/dd-trace/src/profiling/config.js +6 -14
  37. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -24
  38. package/packages/dd-trace/src/profiling/profiler.js +2 -0
  39. package/packages/dd-trace/src/startup-log.js +1 -0
package/index.d.ts CHANGED
@@ -295,7 +295,18 @@ interface Plugins {
295
295
  }
296
296
 
297
297
  declare namespace tracer {
298
- export type SpanOptions = opentracing.SpanOptions;
298
+ export type SpanOptions = Omit<opentracing.SpanOptions, 'childOf'> & {
299
+ /**
300
+ * Set childOf to 'null' to create a root span without a parent, even when a parent span
301
+ * exists in the current async context. If 'undefined' the parent will be inferred from the
302
+ * existing async context.
303
+ */
304
+ childOf?: opentracing.Span | opentracing.SpanContext | null;
305
+ /**
306
+ * Optional name of the integration that crated this span.
307
+ */
308
+ integrationName?: string;
309
+ };
299
310
  export { Tracer };
300
311
 
301
312
  export interface TraceOptions extends Analyzable {
@@ -749,6 +760,14 @@ declare namespace tracer {
749
760
  */
750
761
  dbmPropagationMode?: 'disabled' | 'service' | 'full'
751
762
 
763
+ /**
764
+ * Whether to enable Data Streams Monitoring.
765
+ * Can also be enabled via the DD_DATA_STREAMS_ENABLED environment variable.
766
+ * When not provided, the value of DD_DATA_STREAMS_ENABLED is used.
767
+ * @default false
768
+ */
769
+ dsmEnabled?: boolean
770
+
752
771
  /**
753
772
  * Configuration of the AppSec protection. Can be a boolean as an alias to `appsec.enabled`.
754
773
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.85.0",
3
+ "version": "5.86.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -59,28 +59,46 @@ class DatadogStorage extends AsyncLocalStorage {
59
59
  return stores.get(handle)
60
60
  }
61
61
  }
62
+ }
63
+
64
+ // To handle all versions always correct, feature detect AsyncContextFrame and
65
+ // fallback to manual approach if not active. With ACF `run` delegates to
66
+ // `enterWith`, without ACF `run` does not.
67
+ const isACFActive = (() => {
68
+ let active = false
69
+ const als = new AsyncLocalStorage()
70
+ als.enterWith = () => { active = true }
71
+ als.run(1, () => {})
72
+ als.disable()
73
+ return active
74
+ })()
75
+
76
+ if (!isACFActive) {
77
+ const superGetStore = AsyncLocalStorage.prototype.getStore
78
+ const superEnterWith = AsyncLocalStorage.prototype.enterWith
62
79
 
63
80
  /**
64
- * Here, we replicate the behavior of the original `run()` method. We ensure
65
- * that our `enterWith()` is called internally, so that the handle to the
66
- * store is set. As an optimization, we use super for getStore and enterWith
67
- * when dealing with the parent store, so that we don't have to access the
68
- * WeakMap.
81
+ * Override the `run` method to manually call `enterWith` and `getStore`
82
+ * when not using AsyncContextFrame.
83
+ *
84
+ * Without ACF, super.run() won't call this.enterWith(), so the WeakMap handle
85
+ * is never created and getStore() would fail.
86
+ *
69
87
  * @template R
70
- * @template TArgs = unknown[]
88
+ * @template {unknown[]} TArgs
71
89
  * @param {Store<unknown>} store
72
- * @param {() => R} fn
73
- * @param {...TArgs} args
90
+ * @param {(...args: TArgs) => R} fn
91
+ * @param {TArgs} args
74
92
  * @returns {R}
75
93
  * @override
76
94
  */
77
- run (store, fn, ...args) {
78
- const prior = super.getStore()
95
+ DatadogStorage.prototype.run = function run (store, fn, ...args) {
96
+ const prior = superGetStore.call(this)
79
97
  this.enterWith(store)
80
98
  try {
81
99
  return Reflect.apply(fn, null, args)
82
100
  } finally {
83
- super.enterWith(prior)
101
+ superEnterWith.call(this, prior)
84
102
  }
85
103
  }
86
104
  }
@@ -111,4 +129,4 @@ function storage (namespace) {
111
129
  return storages[namespace]
112
130
  }
113
131
 
114
- module.exports = { storage }
132
+ module.exports = { storage, isACFActive }
@@ -7,6 +7,7 @@ const {
7
7
  getCoveredFilenamesFromCoverage,
8
8
  JEST_WORKER_TRACE_PAYLOAD_CODE,
9
9
  JEST_WORKER_COVERAGE_PAYLOAD_CODE,
10
+ JEST_WORKER_TELEMETRY_PAYLOAD_CODE,
10
11
  getTestLineStart,
11
12
  getTestSuitePath,
12
13
  getTestParametersString,
@@ -16,6 +17,7 @@ const {
16
17
  isModifiedTest,
17
18
  } = require('../../dd-trace/src/plugins/util/test')
18
19
  const {
20
+ SEED_SUFFIX_RE,
19
21
  getFormattedJestTestParameters,
20
22
  getJestTestName,
21
23
  getJestSuitesToRun,
@@ -35,6 +37,7 @@ const testSuiteErrorCh = channel('ci:jest:test-suite:error')
35
37
  const workerReportTraceCh = channel('ci:jest:worker-report:trace')
36
38
  const workerReportCoverageCh = channel('ci:jest:worker-report:coverage')
37
39
  const workerReportLogsCh = channel('ci:jest:worker-report:logs')
40
+ const workerReportTelemetryCh = channel('ci:jest:worker-report:telemetry')
38
41
 
39
42
  const testSuiteCodeCoverageCh = channel('ci:jest:test-suite:code-coverage')
40
43
 
@@ -55,6 +58,7 @@ const itrSkippedSuitesCh = channel('ci:jest:itr:skipped-suites')
55
58
  // Message sent by jest's main process to workers to run a test suite (=test file)
56
59
  // https://github.com/jestjs/jest/blob/1d682f21c7a35da4d3ab3a1436a357b980ebd0fa/packages/jest-worker/src/types.ts#L37
57
60
  const CHILD_MESSAGE_CALL = 1
61
+
58
62
  // Maximum time we'll wait for the tracer to flush
59
63
  const FLUSH_TIMEOUT = 10_000
60
64
 
@@ -515,6 +519,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
515
519
  }
516
520
  }
517
521
  if (event.name === 'test_done') {
522
+ const originalError = event.test?.errors?.[0]
518
523
  let status = 'pass'
519
524
  if (event.test.errors && event.test.errors.length) {
520
525
  status = 'fail'
@@ -594,7 +599,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
594
599
  const shouldSetProbe = this.isDiEnabled && willBeRetriedByFailedTestReplay && numTestExecutions === 1
595
600
  testErrCh.publish({
596
601
  ...ctx.currentStore,
597
- error: formatJestError(event.test.errors[0]),
602
+ error: formatJestError(originalError),
598
603
  shouldSetProbe,
599
604
  promises,
600
605
  finalStatus,
@@ -954,7 +959,7 @@ function getCliWrapper (isNewJestVersion) {
954
959
  isTestManagementTestsEnabled = false
955
960
  testManagementTests = {}
956
961
  } else {
957
- testManagementTests = receivedTestManagementTests
962
+ testManagementTests = receivedTestManagementTests || {}
958
963
  }
959
964
  } catch (err) {
960
965
  log.error('Jest test management tests error', err)
@@ -1012,33 +1017,39 @@ function getCliWrapper (isNewJestVersion) {
1012
1017
  * The rationale behind is the following: you may still be able to block your CI pipeline by gating
1013
1018
  * on flakiness (the test will be considered flaky), but you may choose to unblock the pipeline too.
1014
1019
  */
1020
+ let numEfdFailedTestsToIgnore = 0
1015
1021
  if (isEarlyFlakeDetectionEnabled) {
1016
- let numFailedTestsToIgnore = 0
1017
1022
  for (const testStatuses of newTestsTestStatuses.values()) {
1018
1023
  const { pass, fail } = getTestStats(testStatuses)
1019
1024
  if (pass > 0) { // as long as one passes, we'll consider the test passed
1020
- numFailedTestsToIgnore += fail
1025
+ numEfdFailedTestsToIgnore += fail
1021
1026
  }
1022
1027
  }
1023
1028
  // If every test that failed was an EFD retry, we'll consider the suite passed
1024
- if (numFailedTestsToIgnore !== 0 && result.results.numFailedTests === numFailedTestsToIgnore) {
1029
+ if (numEfdFailedTestsToIgnore !== 0 && result.results.numFailedTests === numEfdFailedTestsToIgnore) {
1025
1030
  result.results.success = true
1026
1031
  }
1027
1032
  }
1028
1033
 
1034
+ let numFailedQuarantinedTests = 0
1035
+ let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
1029
1036
  if (isTestManagementTestsEnabled) {
1030
1037
  const failedTests = result
1031
1038
  .results
1032
1039
  .testResults.flatMap(({ testResults, testFilePath: testSuiteAbsolutePath }) => (
1033
1040
  testResults.map(({ fullName: testName, status }) => (
1034
- { testName, testSuiteAbsolutePath, status }
1041
+ {
1042
+ // Strip @fast-check/jest seed suffix so the name matches what was reported via TEST_NAME
1043
+ testName: testSuiteAbsolutePathsWithFastCheck.has(testSuiteAbsolutePath)
1044
+ ? testName.replace(SEED_SUFFIX_RE, '')
1045
+ : testName,
1046
+ testSuiteAbsolutePath,
1047
+ status,
1048
+ }
1035
1049
  ))
1036
1050
  ))
1037
1051
  .filter(({ status }) => status === 'failed')
1038
1052
 
1039
- let numFailedQuarantinedTests = 0
1040
- let numFailedQuarantinedOrDisabledAttemptedToFixTests = 0
1041
-
1042
1053
  for (const { testName, testSuiteAbsolutePath } of failedTests) {
1043
1054
  const testSuite = getTestSuitePath(testSuiteAbsolutePath, result.globalConfig.rootDir)
1044
1055
  const testManagementTest = testManagementTests
@@ -1069,6 +1080,16 @@ function getCliWrapper (isNewJestVersion) {
1069
1080
  }
1070
1081
  }
1071
1082
 
1083
+ // Combined check: if all failed tests are accounted for by EFD (flaky retries) and/or quarantine,
1084
+ // we should consider the suite passed even when neither check alone covers all failures.
1085
+ if (!result.results.success && (isEarlyFlakeDetectionEnabled || isTestManagementTestsEnabled)) {
1086
+ const totalIgnoredFailures =
1087
+ numEfdFailedTestsToIgnore + numFailedQuarantinedTests + numFailedQuarantinedOrDisabledAttemptedToFixTests
1088
+ if (totalIgnoredFailures !== 0 && result.results.numFailedTests === totalIgnoredFailures) {
1089
+ result.results.success = true
1090
+ }
1091
+ }
1092
+
1072
1093
  // Determine session status after EFD and quarantine checks have potentially modified success
1073
1094
  let status, error
1074
1095
  if (result.results.success) {
@@ -1534,6 +1555,10 @@ function onMessageWrapper (onMessage) {
1534
1555
  workerReportLogsCh.publish(data)
1535
1556
  return
1536
1557
  }
1558
+ if (code === JEST_WORKER_TELEMETRY_PAYLOAD_CODE) { // datadog telemetry payload
1559
+ workerReportTelemetryCh.publish(data)
1560
+ return
1561
+ }
1537
1562
  return onMessage.apply(this, arguments)
1538
1563
  }
1539
1564
  }
@@ -165,6 +165,15 @@ function getOnEndHandler (isParallel) {
165
165
  this.failures -= numFailedQuarantinedTests + numFailedRetriedQuarantinedOrDisabledTests
166
166
  }
167
167
 
168
+ // Recompute status after EFD and quarantine adjustments have reduced failure counts
169
+ if (status === 'fail') {
170
+ if (this.stats) {
171
+ status = this.stats.failures === 0 ? 'pass' : 'fail'
172
+ } else {
173
+ status = this.failures === 0 ? 'pass' : 'fail'
174
+ }
175
+ }
176
+
168
177
  if (status === 'fail') {
169
178
  error = new Error(`Failed tests: ${this.failures}.`)
170
179
  }
@@ -4,45 +4,186 @@ const { getEnvironmentVariable } = require('../../dd-trace/src/config/helper')
4
4
  const { channel, addHook } = require('./helpers/instrument')
5
5
  const prismaHelperInit = channel('apm:prisma:helper:init')
6
6
 
7
- const prismaHook = (runtime, versions, name, isIitm) => {
8
- const originalGetPrismaClient = runtime.getPrismaClient
7
+ /**
8
+ * @typedef {object} EnvValue
9
+ * @property {string|undefined} [value]
10
+ * @property {string|null|undefined} [fromEnvVar]
11
+ */
12
+
13
+ /**
14
+ * @typedef {object} DatasourceConfig
15
+ * @property {string|EnvValue|undefined} [url]
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} PrismaRuntimeConfig
20
+ * @property {Record<string, DatasourceConfig>|undefined} [inlineDatasources]
21
+ * @property {Record<string, { url?: string }>|undefined} [overrideDatasources]
22
+ * @property {Record<string, { url?: string }>|undefined} [datasources]
23
+ * @property {string[]|undefined} [datasourceNames]
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} DbConfig
28
+ * @property {string|undefined} [user]
29
+ * @property {string|undefined} [password]
30
+ * @property {string|undefined} [host]
31
+ * @property {string|number|undefined} [port]
32
+ * @property {string|undefined} [database]
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} AdapterConfig
37
+ * @property {string|undefined} [connectionString]
38
+ * @property {string|undefined} [user]
39
+ * @property {string|undefined} [password]
40
+ * @property {string|undefined} [host]
41
+ * @property {string|number|undefined} [port]
42
+ * @property {string|undefined} [database]
43
+ */
44
+
45
+ /**
46
+ * @typedef {object} Adapter
47
+ * @property {AdapterConfig|undefined} [config]
48
+ * @property {{ options?: AdapterConfig }|undefined} [externalPool]
49
+ */
50
+
51
+ /**
52
+ * @typedef {object} PrismaClientConfig
53
+ * @property {string|undefined} [datasourceUrl]
54
+ * @property {Record<string, { url?: string }>|undefined} [datasources]
55
+ * @property {Adapter|undefined} [adapter]
56
+ */
57
+
58
+ /**
59
+ * @typedef {object} PrismaHelperCtx
60
+ * @property {DbConfig} [dbConfig]
61
+ * @property {import('../../datadog-plugin-prisma/src/datadog-tracing-helper')} [helper]
62
+ */
63
+
64
+ /**
65
+ * @param {string|EnvValue|undefined} envValue
66
+ * @returns {string|undefined}
67
+ */
68
+ function resolveEnvValue (envValue) {
69
+ return typeof envValue === 'object' && envValue
70
+ ? (envValue.value || getEnvironmentVariable(envValue.fromEnvVar ?? ''))
71
+ : envValue
72
+ }
73
+
74
+ /**
75
+ * @param {PrismaRuntimeConfig|undefined} config
76
+ * @param {string} datasourceName
77
+ * @returns {string|undefined}
78
+ */
79
+ function resolveDatasourceUrl (config, datasourceName) {
80
+ return resolveEnvValue(config?.inlineDatasources?.[datasourceName]?.url) ??
81
+ config?.overrideDatasources?.[datasourceName]?.url ??
82
+ config?.datasources?.[datasourceName]?.url ??
83
+ getEnvironmentVariable('DATABASE_URL')
84
+ }
85
+
86
+ /**
87
+ * @param {DbConfig} dbConfig
88
+ * @returns {DbConfig|undefined}
89
+ */
90
+ function normalizeDbConfig (dbConfig) {
91
+ dbConfig.port = dbConfig.port == null ? undefined : String(dbConfig.port)
92
+ const hasValues = dbConfig.user || dbConfig.password || dbConfig.host || dbConfig.port || dbConfig.database
93
+ return hasValues ? dbConfig : undefined
94
+ }
95
+
96
+ /**
97
+ * @param {Adapter|undefined} adapter
98
+ * @returns {DbConfig|undefined}
99
+ */
100
+ function resolveAdapterDbConfig (adapter) {
101
+ const adapterConfig = adapter?.config || adapter?.externalPool?.options
102
+ if (!adapterConfig) {
103
+ return
104
+ }
105
+
106
+ if (typeof adapterConfig === 'string') {
107
+ return parseDBString(adapterConfig)
108
+ }
109
+
110
+ const parsed = parseDBString(adapterConfig.connectionString)
111
+ if (parsed) {
112
+ return parsed
113
+ }
114
+
115
+ return normalizeDbConfig({
116
+ user: adapterConfig.user,
117
+ password: adapterConfig.password,
118
+ host: adapterConfig.host,
119
+ port: adapterConfig.port,
120
+ database: adapterConfig.database,
121
+ })
122
+ }
123
+
124
+ /**
125
+ * @param {PrismaClientConfig|undefined} clientConfig
126
+ * @param {string} datasourceName
127
+ * @param {DbConfig|undefined} runtimeDbConfig
128
+ * @returns {DbConfig|undefined}
129
+ */
130
+ function resolveClientDbConfig (clientConfig, datasourceName, runtimeDbConfig) {
131
+ return resolveAdapterDbConfig(clientConfig?.adapter) ||
132
+ parseDBString(clientConfig?.datasources?.[datasourceName]?.url ?? clientConfig?.datasourceUrl) ||
133
+ runtimeDbConfig
134
+ }
9
135
 
10
- if (!originalGetPrismaClient) return runtime
136
+ /**
137
+ * @param {unknown} runtime
138
+ * @param {string} versions
139
+ * @param {string} [name]
140
+ * @param {boolean} [isIitm]
141
+ * @returns {object}
142
+ */
143
+ const prismaHook = (runtime, versions, name, isIitm) => {
144
+ /**
145
+ * @typedef {{ getPrismaClient?: (config: PrismaRuntimeConfig, ...args: unknown[]) => Function }} PrismaRuntime
146
+ */
147
+ const prismaRuntime = /** @type {PrismaRuntime} */ (runtime)
148
+ const originalGetPrismaClient = prismaRuntime.getPrismaClient
11
149
 
12
- const prismaHelperCtx = {}
150
+ if (!originalGetPrismaClient) {
151
+ return runtime
152
+ }
13
153
 
154
+ /**
155
+ * @param {PrismaRuntimeConfig|undefined} config
156
+ */
14
157
  const wrappedGetPrismaClient = function (config) {
15
- // Prisma config shapes vary by version/runtime entrypoint. We try a few known locations
16
- // and fall back to DATABASE_URL when present.
17
- const datasourceUrl =
18
- config?.inlineDatasources?.db?.url?.value ??
19
- config?.inlineDatasources?.db?.url ??
20
- config?.overrideDatasources?.db?.url ??
21
- config?.datasources?.db?.url ??
22
- config?.datasourceUrl ??
23
- getEnvironmentVariable('DATABASE_URL')
24
-
25
- if (datasourceUrl && !prismaHelperCtx.dbConfig) {
26
- prismaHelperCtx.dbConfig = parseDBString(datasourceUrl)
27
- }
28
- prismaHelperInit.publish(prismaHelperCtx)
158
+ const datasourceName = config?.datasourceNames?.[0] || 'db'
159
+ const runtimeDatasourceUrl = resolveDatasourceUrl(config, datasourceName)
160
+ const runtimeDbConfig = parseDBString(runtimeDatasourceUrl)
29
161
 
30
162
  const PrismaClient = originalGetPrismaClient.call(this, config)
31
163
  return class WrappedPrismaClientClass extends PrismaClient {
32
164
  constructor (clientConfig) {
33
165
  super(clientConfig)
34
- this._tracingHelper = prismaHelperCtx.helper
35
- this._engine.tracingHelper = prismaHelperCtx.helper
166
+ /**
167
+ * @type {PrismaHelperCtx}
168
+ */
169
+ const prismaHelperCtx = {
170
+ dbConfig: resolveClientDbConfig(clientConfig, datasourceName, runtimeDbConfig),
171
+ }
172
+ prismaHelperInit.publish(prismaHelperCtx)
173
+
174
+ const helper = prismaHelperCtx.helper
175
+ this._tracingHelper = helper
176
+ this._engine.tracingHelper = helper
36
177
  }
37
178
  }
38
179
  }
39
180
 
40
181
  if (isIitm) {
41
- runtime.getPrismaClient = wrappedGetPrismaClient
182
+ prismaRuntime.getPrismaClient = wrappedGetPrismaClient
42
183
  return runtime
43
184
  }
44
185
 
45
- return new Proxy(runtime, {
186
+ return new Proxy(prismaRuntime, {
46
187
  get (target, prop) {
47
188
  if (prop === 'getPrismaClient') {
48
189
  return wrappedGetPrismaClient
@@ -62,14 +203,68 @@ for (const config of prismaConfigs) {
62
203
  addHook(config, prismaHook)
63
204
  }
64
205
 
206
+ /**
207
+ * @param {string|undefined} dbString
208
+ * @returns {DbConfig|undefined}
209
+ */
65
210
  function parseDBString (dbString) {
66
- const url = new URL(dbString)
67
- const dbConfig = {
68
- user: url.username,
69
- password: url.password,
70
- host: url.hostname,
71
- port: url.port,
72
- database: url.pathname.slice(1), // Remove leading slash
211
+ if (!dbString || typeof dbString !== 'string') {
212
+ return
213
+ }
214
+
215
+ const sqlServerConfig = parseSqlServerConnectionString(dbString)
216
+ if (sqlServerConfig) {
217
+ return sqlServerConfig
218
+ }
219
+
220
+ try {
221
+ const url = new URL(dbString)
222
+ return normalizeDbConfig({
223
+ user: url.username,
224
+ password: url.password,
225
+ host: url.hostname,
226
+ port: url.port,
227
+ database: url.pathname?.slice(1) || undefined,
228
+ })
229
+ } catch {}
230
+ }
231
+
232
+ /**
233
+ * @param {string} dbString
234
+ * @returns {DbConfig|undefined}
235
+ */
236
+ function parseSqlServerConnectionString (dbString) {
237
+ if (!dbString.startsWith('sqlserver://')) {
238
+ return
239
+ }
240
+ const segments = dbString.slice(12).split(';').filter(Boolean)
241
+ if (!segments.length) {
242
+ return
243
+ }
244
+
245
+ let hostPart = segments.shift()
246
+ let user
247
+ let password
248
+ if (hostPart?.includes('@')) {
249
+ const [userInfo, hostInfo] = hostPart.split('@')
250
+ hostPart = hostInfo
251
+ ;[user, password] = userInfo.split(':')
73
252
  }
74
- return dbConfig
253
+
254
+ let database
255
+ for (const segment of segments) {
256
+ const [rawKey, ...rawValue] = segment.split('=')
257
+ const value = rawValue.join('=').trim()
258
+ const key = rawKey?.trim().toLowerCase()
259
+ if (!key || !value) {
260
+ continue
261
+ }
262
+ if (key === 'database' || key === 'databasename' || key === 'db') database = value
263
+ else if (key === 'user' || key === 'username' || key === 'uid') user = value
264
+ else if (key === 'password' || key === 'pwd') password = value
265
+ }
266
+
267
+ const [host, port] = hostPart?.split(':') ?? []
268
+
269
+ return normalizeDbConfig({ user, password, host, port, database })
75
270
  }
@@ -13,6 +13,7 @@ const producerCh = tracingChannel('ws:send')
13
13
  const receiverCh = tracingChannel('ws:receive')
14
14
  const closeCh = tracingChannel('ws:close')
15
15
  const emitCh = channel('tracing:ws:server:connect:emit')
16
+ const setSocketCh = channel('tracing:ws:server:connect:setSocket')
16
17
  // TODO: Add a error channel / handle error events properly.
17
18
 
18
19
  /**
@@ -181,12 +182,33 @@ addHook({
181
182
  return ws
182
183
  })
183
184
 
185
+ /**
186
+ * Prevent internal event handlers (data, close, etc.) registered by the ws library to
187
+ * capture the connection span in their async context. Otherwise, the
188
+ * finished connection span is retained for the entire lifetime of the WebSocket
189
+ * (via ACF -> handle -> WeakMap).
190
+ *
191
+ * @param {Function} setSocket
192
+ * @returns {(...args: unknown[]) => unknown}
193
+ */
194
+ function wrapSetSocket (setSocket) {
195
+ return function wrappedSetSocket (...args) {
196
+ if (!setSocketCh.hasSubscribers) {
197
+ return setSocket.apply(this, args)
198
+ }
199
+ return setSocketCh.runStores({}, () => {
200
+ return setSocket.apply(this, args)
201
+ })
202
+ }
203
+ }
204
+
184
205
  addHook({
185
206
  name: 'ws',
186
207
  file: 'lib/websocket.js',
187
208
  versions: ['>=8.0.0'],
188
209
  }, moduleExports => {
189
210
  const ws = /** @type {WebSocketClass} */ (moduleExports)
211
+ shimmer.wrap(ws.prototype, 'setSocket', wrapSetSocket)
190
212
  shimmer.wrap(ws.prototype, 'send', wrapSend)
191
213
  shimmer.wrap(ws.prototype, 'close', wrapClose)
192
214
 
@@ -13,7 +13,6 @@ const {
13
13
  isModifiedTest,
14
14
  CUCUMBER_IS_PARALLEL,
15
15
  ITR_CORRELATION_ID,
16
- TEST_BROWSER_DRIVER,
17
16
  TEST_CODE_OWNERS,
18
17
  TEST_EARLY_FLAKE_ABORT_REASON,
19
18
  TEST_EARLY_FLAKE_ENABLED,
@@ -21,7 +20,6 @@ const {
21
20
  TEST_IS_MODIFIED,
22
21
  TEST_IS_NEW,
23
22
  TEST_IS_RETRY,
24
- TEST_IS_RUM_ACTIVE,
25
23
  TEST_ITR_FORCED_RUN,
26
24
  TEST_ITR_UNSKIPPABLE,
27
25
  TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
@@ -108,7 +106,9 @@ class CucumberPlugin extends CiPlugin {
108
106
  this.testModuleSpan.finish()
109
107
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
110
108
  this.testSessionSpan.finish()
111
- this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
109
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session', {
110
+ hasFailedTestReplay: this.libraryConfig?.isDiEnabled || undefined,
111
+ })
112
112
  finishAllTraceSpans(this.testSessionSpan)
113
113
  this.telemetry.count(TELEMETRY_TEST_SESSION, {
114
114
  provider: this.ciProviderName,
@@ -362,13 +362,7 @@ class CucumberPlugin extends CiPlugin {
362
362
  }
363
363
  }
364
364
 
365
- const spanTags = span.context()._tags
366
- const telemetryTags = {
367
- hasCodeOwners: !!spanTags[TEST_CODE_OWNERS],
368
- isNew,
369
- isRum: spanTags[TEST_IS_RUM_ACTIVE] === 'true',
370
- browserDriver: spanTags[TEST_BROWSER_DRIVER],
371
- }
365
+ const telemetryTags = this.getTestTelemetryTags(span)
372
366
  span.finish()
373
367
  if (!isStep) {
374
368
  this.telemetry.ciVisEvent(
@@ -976,11 +976,15 @@ class CypressPlugin {
976
976
  this.finishedTestsByFile[testSuite] = [finishedTest]
977
977
  }
978
978
  // test spans are finished at after:spec
979
+ const activeSpanTags = this.activeTestSpan.context()._tags
979
980
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test', {
980
- hasCodeOwners: !!this.activeTestSpan.context()._tags[TEST_CODE_OWNERS],
981
+ hasCodeOwners: !!activeSpanTags[TEST_CODE_OWNERS],
981
982
  isNew,
982
983
  isRum: isRUMActive,
983
984
  browserDriver: 'cypress',
985
+ isQuarantined: isQuarantinedFromSupport,
986
+ isModified,
987
+ isDisabled: activeSpanTags[TEST_MANAGEMENT_IS_DISABLED] === 'true',
984
988
  })
985
989
  this.activeTestSpan = null
986
990
 
@@ -127,9 +127,10 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
127
127
  const meta = {
128
128
  'gcloud.project_id': subscription.pubsub.projectId,
129
129
  'pubsub.topic': topic,
130
+ 'pubsub.message_id': message.messageId,
130
131
  'span.kind': 'consumer',
132
+ 'pubsub.subscription': subscription.name,
131
133
  'pubsub.subscription_type': 'pull',
132
- 'pubsub.span_type': 'message_processing',
133
134
  'messaging.operation': 'receive',
134
135
  base_service: baseService,
135
136
  service_override_type: 'custom',
@@ -171,9 +172,6 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
171
172
  metrics,
172
173
  }, ctx)
173
174
 
174
- if (message.id) {
175
- span.setTag('pubsub.message_id', message.id)
176
- }
177
175
  if (message.publishTime) {
178
176
  span.setTag('pubsub.publish_time', message.publishTime.toISOString())
179
177
  }