dd-trace 5.94.0 → 5.95.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/LICENSE-3rdparty.csv +46 -44
  2. package/index.d.ts +182 -13
  3. package/package.json +1 -1
  4. package/packages/datadog-instrumentations/src/anthropic.js +1 -1
  5. package/packages/datadog-instrumentations/src/helpers/rewriter/{orchestrion/compiler.js → compiler.js} +4 -13
  6. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +16 -2
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/langgraph.js +2 -2
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/{orchestrion/transforms.js → transforms.js} +3 -89
  9. package/packages/datadog-plugin-dd-trace-api/src/index.js +1 -4
  10. package/packages/dd-trace/src/azure_metadata.js +15 -15
  11. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +73 -1
  12. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +76 -1
  13. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +259 -0
  14. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +56 -0
  15. package/packages/dd-trace/src/config/config-base.d.ts +7 -0
  16. package/packages/dd-trace/src/config/config-base.js +5 -0
  17. package/packages/dd-trace/src/config/config-types.d.ts +78 -0
  18. package/packages/dd-trace/src/config/generated-config-types.d.ts +582 -0
  19. package/packages/dd-trace/src/config/supported-configurations.json +7 -0
  20. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  21. package/packages/dd-trace/src/llmobs/constants/text.js +1 -1
  22. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  23. package/packages/dd-trace/src/llmobs/plugins/anthropic.js +11 -2
  24. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +4 -1
  25. package/packages/dd-trace/src/llmobs/writers/spans.js +1 -1
  26. package/packages/dd-trace/src/priority_sampler.js +1 -1
  27. package/packages/dd-trace/src/rate_limiter.js +2 -1
  28. package/packages/dd-trace/src/tagger.js +31 -35
  29. package/vendor/dist/@apm-js-collab/code-transformer/LICENSE +28 -0
  30. package/vendor/dist/@apm-js-collab/code-transformer/index.js +133 -0
  31. package/vendor/dist/@opentelemetry/core/index.js +1 -1
  32. package/vendor/dist/@opentelemetry/resources/index.js +1 -1
  33. package/vendor/dist/esquery/index.js +1 -1
  34. package/vendor/dist/meriyah/index.js +1 -1
  35. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +0 -43
  36. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +0 -49
  37. package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +0 -121
  38. package/vendor/dist/astring/LICENSE +0 -19
  39. package/vendor/dist/astring/index.js +0 -1
@@ -3,14 +3,10 @@
3
3
  const Plugin = require('../../dd-trace/src/plugins/plugin')
4
4
  const telemetryMetrics = require('../../dd-trace/src/telemetry/metrics')
5
5
  const apiMetrics = telemetryMetrics.manager.namespace('tracers')
6
- const { getValueFromEnvSources } = require('../../dd-trace/src/config/helper')
7
6
 
8
7
  // api ==> here
9
8
  const objectMap = new WeakMap()
10
9
 
11
- const injectionEnabledTag =
12
- `injection_enabled:${getValueFromEnvSources('DD_INJECTION_ENABLED') ? 'yes' : 'no'}`
13
-
14
10
  module.exports = class DdTraceApiPlugin extends Plugin {
15
11
  static id = 'dd-trace-api'
16
12
 
@@ -18,6 +14,7 @@ module.exports = class DdTraceApiPlugin extends Plugin {
18
14
  super(...args)
19
15
 
20
16
  const tracer = this._tracer
17
+ const injectionEnabledTag = `injection_enabled:${this._tracerConfig.injectionEnabled ? 'yes' : 'no'}`
21
18
 
22
19
  this.addSub('datadog-api:v1:tracerinit', ({ proxy }) => {
23
20
  const proxyVal = proxy()
@@ -5,10 +5,9 @@
5
5
  const os = require('os')
6
6
  const {
7
7
  getEnvironmentVariable,
8
- getEnvironmentVariables,
9
8
  getValueFromEnvSources,
10
9
  } = require('./config/helper')
11
- const { getIsAzureFunction, getIsFlexConsumptionAzureFunction } = require('./serverless')
10
+ const { getIsAzureFunction } = require('./serverless')
12
11
 
13
12
  function extractSubscriptionID (ownerName) {
14
13
  if (ownerName !== undefined) {
@@ -46,32 +45,33 @@ function trimObject (obj) {
46
45
  }
47
46
 
48
47
  function buildMetadata () {
49
- const {
50
- COMPUTERNAME,
51
- FUNCTIONS_EXTENSION_VERSION,
52
- FUNCTIONS_WORKER_RUNTIME,
53
- FUNCTIONS_WORKER_RUNTIME_VERSION,
54
- WEBSITE_INSTANCE_ID,
55
- WEBSITE_OWNER_NAME,
56
- WEBSITE_OS,
57
- WEBSITE_RESOURCE_GROUP,
58
- WEBSITE_SITE_NAME,
59
- } = getEnvironmentVariables()
48
+ const COMPUTERNAME = getEnvironmentVariable('COMPUTERNAME')
49
+ const FUNCTIONS_EXTENSION_VERSION = getEnvironmentVariable('FUNCTIONS_EXTENSION_VERSION')
50
+ const FUNCTIONS_WORKER_RUNTIME = getEnvironmentVariable('FUNCTIONS_WORKER_RUNTIME')
51
+ const FUNCTIONS_WORKER_RUNTIME_VERSION = getEnvironmentVariable('FUNCTIONS_WORKER_RUNTIME_VERSION')
52
+ const WEBSITE_INSTANCE_ID = getEnvironmentVariable('WEBSITE_INSTANCE_ID')
53
+ const WEBSITE_OWNER_NAME = getEnvironmentVariable('WEBSITE_OWNER_NAME')
54
+ const WEBSITE_OS = getEnvironmentVariable('WEBSITE_OS')
55
+ const WEBSITE_RESOURCE_GROUP = getEnvironmentVariable('WEBSITE_RESOURCE_GROUP')
56
+ const WEBSITE_SITE_NAME = getEnvironmentVariable('WEBSITE_SITE_NAME')
57
+ const WEBSITE_SKU = getEnvironmentVariable('WEBSITE_SKU')
60
58
 
61
59
  const DD_AZURE_RESOURCE_GROUP = getValueFromEnvSources('DD_AZURE_RESOURCE_GROUP')
60
+ const isAzureFunction = FUNCTIONS_EXTENSION_VERSION !== undefined && FUNCTIONS_WORKER_RUNTIME !== undefined
61
+ const isFlexConsumptionAzureFunction = isAzureFunction && WEBSITE_SKU === 'FlexConsumption'
62
62
 
63
63
  const subscriptionID = extractSubscriptionID(WEBSITE_OWNER_NAME)
64
64
 
65
65
  const siteName = WEBSITE_SITE_NAME
66
66
 
67
- const [siteKind, siteType] = getIsAzureFunction()
67
+ const [siteKind, siteType] = isAzureFunction
68
68
  ? ['functionapp', 'function']
69
69
  : ['app', 'app']
70
70
 
71
71
  // Azure Functions on Flex Consumption plans require the `DD_AZURE_RESOURCE_GROUP` env var.
72
72
  // If this logic ever changes, update the logic in `libdatadog`, `serverless-components/src/datadog-trace-agent`,
73
73
  // and the serverless compat layers accordingly.
74
- const resourceGroup = getIsFlexConsumptionAzureFunction()
74
+ const resourceGroup = isFlexConsumptionAzureFunction
75
75
  ? (DD_AZURE_RESOURCE_GROUP ?? WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME))
76
76
  : (WEBSITE_RESOURCE_GROUP ?? extractResourceGroup(WEBSITE_OWNER_NAME))
77
77
 
@@ -16,12 +16,17 @@ const {
16
16
  } = require('../../ci-visibility/telemetry')
17
17
 
18
18
  const { getNumFromKnownTests } = require('../../plugins/util/test')
19
+ const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
19
20
 
20
21
  const MAX_KNOWN_TESTS_PAGES = 10_000
21
22
 
22
23
  /**
23
24
  * Deep-merges page tests into aggregate.
24
25
  * Structure: { module: { suite: [testName, ...] } }
26
+ *
27
+ * @param {object | null} aggregate
28
+ * @param {object | null} page
29
+ * @returns {object | null}
25
30
  */
26
31
  function mergeKnownTests (aggregate, page) {
27
32
  if (!page) return aggregate
@@ -62,6 +67,71 @@ function getKnownTests ({
62
67
  runtimeName,
63
68
  runtimeVersion,
64
69
  custom,
70
+ }, done) {
71
+ const cacheKey = buildCacheKey('known-tests', [
72
+ sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture,
73
+ runtimeName, runtimeVersion, custom,
74
+ ])
75
+
76
+ withCache(cacheKey, (activeCacheKey, cb) => {
77
+ fetchFromApi({
78
+ url,
79
+ isEvpProxy,
80
+ evpProxyPrefix,
81
+ isGzipCompatible,
82
+ env,
83
+ service,
84
+ repositoryUrl,
85
+ sha,
86
+ osVersion,
87
+ osPlatform,
88
+ osArchitecture,
89
+ runtimeName,
90
+ runtimeVersion,
91
+ custom,
92
+ cacheKey: activeCacheKey,
93
+ }, cb)
94
+ }, done)
95
+ }
96
+
97
+ /**
98
+ * Fetches known tests from the API with cursor-based pagination and writes the
99
+ * result to cache on success.
100
+ *
101
+ * @param {object} params
102
+ * @param {string} params.url
103
+ * @param {boolean} params.isEvpProxy
104
+ * @param {string} params.evpProxyPrefix
105
+ * @param {boolean} params.isGzipCompatible
106
+ * @param {string} params.env
107
+ * @param {string} params.service
108
+ * @param {string} params.repositoryUrl
109
+ * @param {string} params.sha
110
+ * @param {string} params.osVersion
111
+ * @param {string} params.osPlatform
112
+ * @param {string} params.osArchitecture
113
+ * @param {string} params.runtimeName
114
+ * @param {string} params.runtimeVersion
115
+ * @param {object} [params.custom]
116
+ * @param {string | null} params.cacheKey
117
+ * @param {Function} done
118
+ */
119
+ function fetchFromApi ({
120
+ url,
121
+ isEvpProxy,
122
+ evpProxyPrefix,
123
+ isGzipCompatible,
124
+ env,
125
+ service,
126
+ repositoryUrl,
127
+ sha,
128
+ osVersion,
129
+ osPlatform,
130
+ osArchitecture,
131
+ runtimeName,
132
+ runtimeVersion,
133
+ custom,
134
+ cacheKey,
65
135
  }, done) {
66
136
  const options = {
67
137
  path: '/api/v2/ci/libraries/tests',
@@ -166,7 +236,9 @@ function getKnownTests ({
166
236
  distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_TESTS, {}, numTests)
167
237
  distributionMetric(TELEMETRY_KNOWN_TESTS_RESPONSE_BYTES, {}, totalResponseBytes)
168
238
 
169
- log.debug('Number of received known tests:', numTests)
239
+ log.debug('Number of received known tests: %d', numTests)
240
+
241
+ writeToCache(cacheKey, aggregateTests)
170
242
 
171
243
  done(null, aggregateTests)
172
244
  } catch (err) {
@@ -13,6 +13,7 @@ const {
13
13
  TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS,
14
14
  TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES,
15
15
  } = require('../../ci-visibility/telemetry')
16
+ const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
16
17
 
17
18
  function getSkippableSuites ({
18
19
  url,
@@ -30,6 +31,76 @@ function getSkippableSuites ({
30
31
  runtimeVersion,
31
32
  custom,
32
33
  testLevel = 'suite',
34
+ }, done) {
35
+ const cacheKey = buildCacheKey('skippable', [
36
+ sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture,
37
+ runtimeName, runtimeVersion, testLevel, custom,
38
+ ])
39
+
40
+ withCache(cacheKey, (activeCacheKey, cb) => {
41
+ fetchFromApi({
42
+ url,
43
+ isEvpProxy,
44
+ evpProxyPrefix,
45
+ isGzipCompatible,
46
+ env,
47
+ service,
48
+ repositoryUrl,
49
+ sha,
50
+ osVersion,
51
+ osPlatform,
52
+ osArchitecture,
53
+ runtimeName,
54
+ runtimeVersion,
55
+ custom,
56
+ testLevel,
57
+ cacheKey: activeCacheKey,
58
+ }, cb)
59
+ }, (err, data) => {
60
+ if (err) return done(err)
61
+ done(null, data.skippableSuites, data.correlationId)
62
+ })
63
+ }
64
+
65
+ /**
66
+ * Fetches skippable suites from the API and writes the result to cache on success.
67
+ *
68
+ * @param {object} params
69
+ * @param {string} params.url
70
+ * @param {boolean} params.isEvpProxy
71
+ * @param {string} params.evpProxyPrefix
72
+ * @param {boolean} params.isGzipCompatible
73
+ * @param {string} params.env
74
+ * @param {string} params.service
75
+ * @param {string} params.repositoryUrl
76
+ * @param {string} params.sha
77
+ * @param {string} params.osVersion
78
+ * @param {string} params.osPlatform
79
+ * @param {string} params.osArchitecture
80
+ * @param {string} params.runtimeName
81
+ * @param {string} params.runtimeVersion
82
+ * @param {object} [params.custom]
83
+ * @param {string} [params.testLevel]
84
+ * @param {string | null} params.cacheKey
85
+ * @param {Function} done
86
+ */
87
+ function fetchFromApi ({
88
+ url,
89
+ isEvpProxy,
90
+ evpProxyPrefix,
91
+ isGzipCompatible,
92
+ env,
93
+ service,
94
+ repositoryUrl,
95
+ sha,
96
+ osVersion,
97
+ osPlatform,
98
+ osArchitecture,
99
+ runtimeName,
100
+ runtimeVersion,
101
+ custom,
102
+ testLevel,
103
+ cacheKey,
33
104
  }, done) {
34
105
  const options = {
35
106
  path: '/api/v2/ci/tests/skippable',
@@ -109,7 +180,11 @@ function getSkippableSuites ({
109
180
  )
110
181
  distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length)
111
182
  log.debug('Number of received skippable %ss:', testLevel, skippableSuites.length)
112
- done(null, skippableSuites, correlationId)
183
+
184
+ const result = { skippableSuites, correlationId }
185
+ writeToCache(cacheKey, result)
186
+
187
+ done(null, result)
113
188
  } catch (err) {
114
189
  done(err)
115
190
  }
@@ -0,0 +1,259 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+ const { createHash } = require('node:crypto')
6
+ const { tmpdir } = require('node:os')
7
+
8
+ const log = require('../../log')
9
+ const { getValueFromEnvSources } = require('../../config/helper')
10
+
11
+ const CACHE_TTL_MS = 30 * 60 * 1000 // 30 minutes
12
+ const CACHE_LOCK_POLL_MS = 500
13
+ const CACHE_LOCK_TIMEOUT_MS = 120_000 // 2 minutes
14
+ const CACHE_LOCK_HEARTBEAT_MS = 30_000 // 30 seconds
15
+
16
+ /**
17
+ * Returns whether the filesystem cache is enabled via the env var.
18
+ *
19
+ * @returns {boolean}
20
+ */
21
+ function isCacheEnabled () {
22
+ const { isTrue } = require('../../util')
23
+ return isTrue(getValueFromEnvSources('DD_EXPERIMENTAL_TEST_REQUESTS_FS_CACHE'))
24
+ }
25
+
26
+ /**
27
+ * Builds a deterministic cache key by hashing arbitrary key-value parts.
28
+ *
29
+ * @param {string} prefix - Cache file prefix (e.g. 'known-tests', 'skippable', 'test-mgmt')
30
+ * @param {Array<unknown>} parts - Values that uniquely identify the cached response
31
+ * @returns {string}
32
+ */
33
+ function buildCacheKey (prefix, parts) {
34
+ const hash = createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 16)
35
+ return `${prefix}-${hash}`
36
+ }
37
+
38
+ /**
39
+ * @param {string} cacheKey
40
+ * @returns {string}
41
+ */
42
+ function getCachePath (cacheKey) {
43
+ return path.join(tmpdir(), `dd-${cacheKey}.json`)
44
+ }
45
+
46
+ /**
47
+ * @param {string} cacheKey
48
+ * @returns {string}
49
+ */
50
+ function getLockPath (cacheKey) {
51
+ return path.join(tmpdir(), `dd-${cacheKey}.lock`)
52
+ }
53
+
54
+ /**
55
+ * Attempts to read cached data from the filesystem.
56
+ *
57
+ * @param {string} cacheKey
58
+ * @returns {{ data: unknown } | undefined}
59
+ */
60
+ function readFromCache (cacheKey) {
61
+ const cachePath = getCachePath(cacheKey)
62
+ try {
63
+ const raw = fs.readFileSync(cachePath, 'utf8')
64
+ const parsed = JSON.parse(raw)
65
+ if (!Object.hasOwn(parsed, 'data')) {
66
+ log.debug('%s cache file has no data field, ignoring', cacheKey)
67
+ return
68
+ }
69
+ const { timestamp, data } = parsed
70
+ if (Date.now() - timestamp > CACHE_TTL_MS) {
71
+ log.debug('%s cache expired (age: %d ms)', cacheKey, Date.now() - timestamp)
72
+ return
73
+ }
74
+ log.debug('%s cache hit', cacheKey)
75
+ return { data }
76
+ } catch {
77
+ // Cache file missing, corrupt, or unreadable — treat as cache miss
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Writes data to the filesystem cache atomically.
83
+ *
84
+ * @param {string} cacheKey
85
+ * @param {unknown} data
86
+ */
87
+ function writeToCache (cacheKey, data) {
88
+ if (!cacheKey) return
89
+ const cachePath = getCachePath(cacheKey)
90
+ const tmpPath = cachePath + '.tmp.' + process.pid
91
+ try {
92
+ fs.writeFileSync(tmpPath, JSON.stringify({ timestamp: Date.now(), data }), 'utf8')
93
+ fs.renameSync(tmpPath, cachePath)
94
+ log.debug('Cache written: %s', cachePath)
95
+ } catch (err) {
96
+ log.error('Failed to write cache %s: %s', cacheKey, err.message)
97
+ try { fs.unlinkSync(tmpPath) } catch { /* ignore */ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Attempts to acquire an exclusive lock using O_CREAT|O_EXCL.
103
+ *
104
+ * @param {string} cacheKey
105
+ * @returns {boolean}
106
+ */
107
+ function tryAcquireLock (cacheKey) {
108
+ const lockPath = getLockPath(cacheKey)
109
+ try {
110
+ const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY)
111
+ fs.writeSync(fd, String(Date.now()))
112
+ fs.closeSync(fd)
113
+ return true
114
+ } catch {
115
+ return false
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Removes the lock file.
121
+ *
122
+ * @param {string} cacheKey
123
+ */
124
+ function releaseLock (cacheKey) {
125
+ try { fs.unlinkSync(getLockPath(cacheKey)) } catch { /* ignore */ }
126
+ }
127
+
128
+ /**
129
+ * Updates the lock file timestamp so waiters know the owner is still alive.
130
+ *
131
+ * @param {string} cacheKey
132
+ */
133
+ function touchLock (cacheKey) {
134
+ const lockPath = getLockPath(cacheKey)
135
+ const tmpPath = lockPath + '.tmp.' + process.pid
136
+ try {
137
+ fs.writeFileSync(tmpPath, String(Date.now()))
138
+ fs.renameSync(tmpPath, lockPath)
139
+ } catch {
140
+ try { fs.unlinkSync(tmpPath) } catch { /* ignore */ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Starts a periodic heartbeat that touches the lock file.
146
+ * Returns a function that stops the heartbeat and releases the lock.
147
+ *
148
+ * @param {string} cacheKey
149
+ * @returns {Function}
150
+ */
151
+ function startLockHeartbeat (cacheKey) {
152
+ const interval = setInterval(() => touchLock(cacheKey), CACHE_LOCK_HEARTBEAT_MS)
153
+ interval.unref()
154
+ return () => {
155
+ clearInterval(interval)
156
+ try { fs.unlinkSync(getLockPath(cacheKey)) } catch { /* ignore */ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Checks whether the lock file is stale (older than the lock timeout).
162
+ *
163
+ * @param {string} cacheKey
164
+ * @returns {boolean}
165
+ */
166
+ function isLockStale (cacheKey) {
167
+ try {
168
+ const content = fs.readFileSync(getLockPath(cacheKey), 'utf8')
169
+ return Date.now() - Number(content) > CACHE_LOCK_TIMEOUT_MS
170
+ } catch {
171
+ return true
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Polls until the cache file appears or the timeout is reached.
177
+ *
178
+ * @param {string} cacheKey
179
+ * @param {Function} fetchFn - function(done) that fetches from the API
180
+ * @param {Function} done - callback(err, ...results)
181
+ */
182
+ function waitForCache (cacheKey, fetchFn, done) {
183
+ const poll = () => {
184
+ const cached = readFromCache(cacheKey)
185
+ if (cached) {
186
+ return done(null, cached.data)
187
+ }
188
+ if (isLockStale(cacheKey)) {
189
+ log.debug('%s lock is stale, attempting takeover', cacheKey)
190
+ releaseLock(cacheKey)
191
+ if (!tryAcquireLock(cacheKey)) {
192
+ return setTimeout(poll, CACHE_LOCK_POLL_MS)
193
+ }
194
+
195
+ const cachedAfterTakeover = readFromCache(cacheKey)
196
+ if (cachedAfterTakeover) {
197
+ releaseLock(cacheKey)
198
+ return done(null, cachedAfterTakeover.data)
199
+ }
200
+
201
+ const stopHeartbeat = startLockHeartbeat(cacheKey)
202
+ return fetchFn((err, ...results) => {
203
+ stopHeartbeat()
204
+ done(err, ...results)
205
+ })
206
+ }
207
+ setTimeout(poll, CACHE_LOCK_POLL_MS)
208
+ }
209
+ poll()
210
+ }
211
+
212
+ /**
213
+ * Wraps a fetch function with filesystem-based caching and cross-process deduplication.
214
+ *
215
+ * When cache is disabled (env var not set), calls fetchFn directly.
216
+ * When enabled, checks cache → acquires lock → fetches → writes cache → releases lock.
217
+ *
218
+ * @param {string} cacheKey - Unique cache key for this request
219
+ * @param {Function} fetchFn - function(cacheKey, done) that performs the API request.
220
+ * Must call writeToCache(cacheKey, data) on success before calling done(null, data).
221
+ * @param {Function} done - callback(err, ...results)
222
+ */
223
+ function withCache (cacheKey, fetchFn, done) {
224
+ if (!isCacheEnabled()) {
225
+ return fetchFn(null, done)
226
+ }
227
+
228
+ // Fast path: cache hit
229
+ const cached = readFromCache(cacheKey)
230
+ if (cached) {
231
+ return done(null, cached.data)
232
+ }
233
+
234
+ // Try to become the fetcher (lock owner)
235
+ const isLockOwner = tryAcquireLock(cacheKey)
236
+
237
+ if (!isLockOwner) {
238
+ log.debug('%s lock held by another process, waiting for cache', cacheKey)
239
+ return waitForCache(cacheKey, (cb) => fetchFn(cacheKey, cb), done)
240
+ }
241
+
242
+ // This process owns the lock — start heartbeat and fetch
243
+ const stopHeartbeat = startLockHeartbeat(cacheKey)
244
+
245
+ fetchFn(cacheKey, (err, ...results) => {
246
+ stopHeartbeat()
247
+ done(err, ...results)
248
+ })
249
+ }
250
+
251
+ module.exports = {
252
+ isCacheEnabled,
253
+ buildCacheKey,
254
+ readFromCache,
255
+ writeToCache,
256
+ withCache,
257
+ getCachePath,
258
+ getLockPath,
259
+ }
@@ -15,6 +15,8 @@ const {
15
15
  TELEMETRY_TEST_MANAGEMENT_TESTS_RESPONSE_BYTES,
16
16
  } = require('../telemetry')
17
17
 
18
+ const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
19
+
18
20
  // Calculate the number of tests from the test management tests response, which has a shape like:
19
21
  // { module: { suites: { suite: { tests: { testName: { properties: {...} } } } } } }
20
22
  function getNumFromTestManagementTests (testManagementTests) {
@@ -48,6 +50,58 @@ function getTestManagementTests ({
48
50
  commitHeadSha,
49
51
  commitHeadMessage,
50
52
  branch,
53
+ }, done) {
54
+ const effectiveSha = commitHeadSha || sha
55
+ const cacheKey = buildCacheKey('test-mgmt', [
56
+ effectiveSha, repositoryUrl, branch,
57
+ ])
58
+
59
+ withCache(cacheKey, (activeCacheKey, cb) => {
60
+ fetchFromApi({
61
+ url,
62
+ isEvpProxy,
63
+ evpProxyPrefix,
64
+ isGzipCompatible,
65
+ repositoryUrl,
66
+ commitMessage,
67
+ sha,
68
+ commitHeadSha,
69
+ commitHeadMessage,
70
+ branch,
71
+ cacheKey: activeCacheKey,
72
+ }, cb)
73
+ }, done)
74
+ }
75
+
76
+ /**
77
+ * Fetches test management tests from the API and writes the result to cache on success.
78
+ *
79
+ * @param {object} params
80
+ * @param {string} params.url
81
+ * @param {boolean} params.isEvpProxy
82
+ * @param {string} params.evpProxyPrefix
83
+ * @param {boolean} params.isGzipCompatible
84
+ * @param {string} params.repositoryUrl
85
+ * @param {string} [params.commitMessage]
86
+ * @param {string} params.sha
87
+ * @param {string} [params.commitHeadSha]
88
+ * @param {string} [params.commitHeadMessage]
89
+ * @param {string} [params.branch]
90
+ * @param {string | null} params.cacheKey
91
+ * @param {Function} done
92
+ */
93
+ function fetchFromApi ({
94
+ url,
95
+ isEvpProxy,
96
+ evpProxyPrefix,
97
+ isGzipCompatible,
98
+ repositoryUrl,
99
+ commitMessage,
100
+ sha,
101
+ commitHeadSha,
102
+ commitHeadMessage,
103
+ branch,
104
+ cacheKey,
51
105
  }, done) {
52
106
  const options = {
53
107
  path: '/api/v2/test/libraries/test-management/tests',
@@ -110,6 +164,8 @@ function getTestManagementTests ({
110
164
 
111
165
  log.debug('Test management tests received: %j', testManagementTests)
112
166
 
167
+ writeToCache(cacheKey, testManagementTests)
168
+
113
169
  done(null, testManagementTests)
114
170
  } catch (err) {
115
171
  done(err)
@@ -0,0 +1,7 @@
1
+ import type { ConfigProperties } from './config-types'
2
+
3
+ declare class ConfigBase {}
4
+
5
+ interface ConfigBase extends ConfigProperties {}
6
+
7
+ export = ConfigBase
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ class ConfigBase {}
4
+
5
+ module.exports = ConfigBase