dd-trace 5.67.0 → 5.69.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 (132) hide show
  1. package/LICENSE-3rdparty.csv +6 -4
  2. package/README.md +0 -2
  3. package/ci/init.js +52 -54
  4. package/ext/exporters.d.ts +2 -1
  5. package/ext/exporters.js +2 -1
  6. package/index.d.ts +240 -3
  7. package/initialize.mjs +1 -1
  8. package/package.json +17 -11
  9. package/packages/datadog-core/src/storage.js +14 -13
  10. package/packages/datadog-esbuild/index.js +118 -26
  11. package/packages/datadog-instrumentations/src/aws-sdk.js +42 -4
  12. package/packages/datadog-instrumentations/src/azure-functions.js +1 -1
  13. package/packages/datadog-instrumentations/src/azure-service-bus.js +1 -1
  14. package/packages/datadog-instrumentations/src/cassandra-driver.js +2 -2
  15. package/packages/datadog-instrumentations/src/connect.js +6 -2
  16. package/packages/datadog-instrumentations/src/cucumber.js +31 -6
  17. package/packages/datadog-instrumentations/src/express.js +5 -6
  18. package/packages/datadog-instrumentations/src/fastify.js +3 -3
  19. package/packages/datadog-instrumentations/src/helpers/hook.js +28 -15
  20. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  21. package/packages/datadog-instrumentations/src/helpers/instrument.js +15 -5
  22. package/packages/datadog-instrumentations/src/helpers/register.js +10 -3
  23. package/packages/datadog-instrumentations/src/http2/client.js +1 -0
  24. package/packages/datadog-instrumentations/src/http2/server.js +0 -1
  25. package/packages/datadog-instrumentations/src/ioredis.js +12 -1
  26. package/packages/datadog-instrumentations/src/jest.js +48 -36
  27. package/packages/datadog-instrumentations/src/limitd-client.js +2 -1
  28. package/packages/datadog-instrumentations/src/mocha/main.js +15 -7
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +3 -0
  30. package/packages/datadog-instrumentations/src/mongoose.js +2 -1
  31. package/packages/datadog-instrumentations/src/oracledb.js +19 -13
  32. package/packages/datadog-instrumentations/src/pg.js +9 -5
  33. package/packages/datadog-instrumentations/src/pino.js +18 -6
  34. package/packages/datadog-instrumentations/src/playwright.js +15 -1
  35. package/packages/datadog-instrumentations/src/sequelize.js +1 -1
  36. package/packages/datadog-instrumentations/src/vitest.js +155 -62
  37. package/packages/datadog-plugin-ai/src/tracing.js +3 -3
  38. package/packages/datadog-plugin-aws-sdk/src/base.js +23 -8
  39. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +2 -2
  40. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +101 -2
  41. package/packages/datadog-plugin-aws-sdk/src/util.js +1 -1
  42. package/packages/datadog-plugin-confluentinc-kafka-javascript/src/index.js +6 -0
  43. package/packages/datadog-plugin-cucumber/src/index.js +4 -56
  44. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +6 -3
  45. package/packages/datadog-plugin-cypress/src/support.js +4 -0
  46. package/packages/datadog-plugin-express/src/code_origin.js +2 -2
  47. package/packages/datadog-plugin-fastify/src/code_origin.js +1 -2
  48. package/packages/datadog-plugin-jest/src/index.js +0 -21
  49. package/packages/datadog-plugin-mocha/src/index.js +3 -57
  50. package/packages/datadog-plugin-mongodb-core/src/index.js +38 -12
  51. package/packages/datadog-plugin-playwright/src/index.js +11 -5
  52. package/packages/datadog-plugin-vitest/src/index.js +5 -1
  53. package/packages/datadog-plugin-ws/src/close.js +1 -1
  54. package/packages/datadog-plugin-ws/src/producer.js +6 -1
  55. package/packages/datadog-plugin-ws/src/receiver.js +6 -1
  56. package/packages/dd-trace/src/aiguard/client.js +25 -0
  57. package/packages/dd-trace/src/aiguard/noop.js +9 -0
  58. package/packages/dd-trace/src/aiguard/sdk.js +173 -0
  59. package/packages/dd-trace/src/aiguard/tags.js +11 -0
  60. package/packages/dd-trace/src/appsec/iast/path-line.js +21 -4
  61. package/packages/dd-trace/src/appsec/iast/security-controls/parser.js +1 -1
  62. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +6 -3
  63. package/packages/dd-trace/src/appsec/stack_trace.js +20 -1
  64. package/packages/dd-trace/src/appsec/telemetry/waf.js +2 -2
  65. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -4
  66. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +11 -3
  67. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +10 -1
  68. package/packages/dd-trace/src/config-helper.js +8 -1
  69. package/packages/dd-trace/src/config.js +92 -304
  70. package/packages/dd-trace/src/config_defaults.js +191 -0
  71. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -1
  72. package/packages/dd-trace/src/datastreams/fnv.js +2 -2
  73. package/packages/dd-trace/src/datastreams/index.js +23 -1
  74. package/packages/dd-trace/src/datastreams/writer.js +3 -2
  75. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  76. package/packages/dd-trace/src/dogstatsd.js +4 -3
  77. package/packages/dd-trace/src/encode/0.4.js +1 -5
  78. package/packages/dd-trace/src/exporter.js +1 -0
  79. package/packages/dd-trace/src/exporters/agent/index.js +3 -2
  80. package/packages/dd-trace/src/exporters/agent/writer.js +1 -1
  81. package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +3 -2
  82. package/packages/dd-trace/src/exporters/common/request.js +2 -1
  83. package/packages/dd-trace/src/exporters/span-stats/index.js +3 -2
  84. package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
  85. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +15 -4
  86. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +20 -7
  87. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +40 -13
  88. package/packages/dd-trace/src/llmobs/plugins/openai.js +7 -1
  89. package/packages/dd-trace/src/llmobs/tagger.js +8 -0
  90. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  91. package/packages/dd-trace/src/log/index.js +27 -16
  92. package/packages/dd-trace/src/log/log.js +29 -5
  93. package/packages/dd-trace/src/log/writer.js +5 -5
  94. package/packages/dd-trace/src/noop/proxy.js +4 -0
  95. package/packages/dd-trace/src/noop/span.js +1 -0
  96. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  97. package/packages/dd-trace/src/opentracing/span.js +19 -5
  98. package/packages/dd-trace/src/payload-tagging/config/index.js +16 -0
  99. package/packages/dd-trace/src/payload-tagging/index.js +26 -15
  100. package/packages/dd-trace/src/payload-tagging/tagging.js +17 -8
  101. package/packages/dd-trace/src/pkg.js +3 -1
  102. package/packages/dd-trace/src/plugin_manager.js +20 -2
  103. package/packages/dd-trace/src/plugins/ci_plugin.js +97 -3
  104. package/packages/dd-trace/src/plugins/composite.js +3 -0
  105. package/packages/dd-trace/src/plugins/index.js +2 -0
  106. package/packages/dd-trace/src/plugins/plugin.js +67 -0
  107. package/packages/dd-trace/src/plugins/util/git-cache.js +129 -0
  108. package/packages/dd-trace/src/plugins/util/git.js +41 -27
  109. package/packages/dd-trace/src/plugins/util/test.js +56 -27
  110. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  111. package/packages/dd-trace/src/priority_sampler.js +70 -46
  112. package/packages/dd-trace/src/profiler.js +4 -1
  113. package/packages/dd-trace/src/profiling/config.js +73 -42
  114. package/packages/dd-trace/src/profiling/profiler.js +3 -1
  115. package/packages/dd-trace/src/profiling/profilers/events.js +3 -8
  116. package/packages/dd-trace/src/profiling/profilers/space.js +1 -0
  117. package/packages/dd-trace/src/profiling/profilers/wall.js +196 -117
  118. package/packages/dd-trace/src/proxy.js +15 -0
  119. package/packages/dd-trace/src/rate_limiter.js +26 -1
  120. package/packages/dd-trace/src/remote_config/capabilities.js +5 -0
  121. package/packages/dd-trace/src/remote_config/manager.js +3 -2
  122. package/packages/dd-trace/src/sampling_rule.js +124 -2
  123. package/packages/dd-trace/src/span_sampler.js +19 -0
  124. package/packages/dd-trace/src/standalone/product.js +9 -0
  125. package/packages/dd-trace/src/standalone/tracesource.js +16 -1
  126. package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +13 -0
  127. package/packages/dd-trace/src/startup-log.js +21 -2
  128. package/packages/dd-trace/src/supported-configurations.json +9 -0
  129. package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
  130. package/packages/dd-trace/src/util.js +1 -1
  131. package/register.js +1 -1
  132. package/version.js +4 -2
@@ -36,6 +36,7 @@ const {
36
36
  getModifiedTestsFromDiff,
37
37
  getPullRequestBaseBranch
38
38
  } = require('./util/test')
39
+ const { getRepositoryRoot } = require('./util/git')
39
40
  const Plugin = require('./plugin')
40
41
  const { COMPONENT } = require('../constants')
41
42
  const log = require('../log')
@@ -61,6 +62,7 @@ const {
61
62
  const { OS_VERSION, OS_PLATFORM, OS_ARCHITECTURE, RUNTIME_NAME, RUNTIME_VERSION } = require('./util/env')
62
63
  const getDiClient = require('../ci-visibility/dynamic-instrumentation')
63
64
  const { DD_MAJOR } = require('../../../../version')
65
+ const id = require('../id')
64
66
 
65
67
  const FRAMEWORK_TO_TRIMMED_COMMAND = {
66
68
  vitest: 'vitest run',
@@ -70,12 +72,44 @@ const FRAMEWORK_TO_TRIMMED_COMMAND = {
70
72
  jest: 'jest'
71
73
  }
72
74
 
75
+ const WORKER_EXPORTER_TO_TEST_FRAMEWORK = {
76
+ vitest_worker: 'vitest',
77
+ jest_worker: 'jest',
78
+ cucumber_worker: 'cucumber',
79
+ mocha_worker: 'mocha',
80
+ playwright_worker: 'playwright'
81
+ }
82
+
83
+ const TEST_FRAMEWORKS_TO_SKIP_GIT_METADATA_EXTRACTION = new Set([
84
+ 'vitest',
85
+ 'jest',
86
+ 'mocha',
87
+ 'cucumber',
88
+ ])
89
+
90
+ function getTestSuiteLevelVisibilityTags (testSuiteSpan, testFramework) {
91
+ const testSuiteSpanContext = testSuiteSpan.context()
92
+
93
+ const suiteTags = {
94
+ [TEST_SUITE_ID]: testSuiteSpanContext.toSpanId(),
95
+ [TEST_SESSION_ID]: testSuiteSpanContext.toTraceId(),
96
+ [TEST_COMMAND]: testSuiteSpanContext._tags[TEST_COMMAND],
97
+ [TEST_MODULE]: testFramework
98
+ }
99
+
100
+ if (testSuiteSpanContext._parentId) {
101
+ suiteTags[TEST_MODULE_ID] = testSuiteSpanContext._parentId.toString(10)
102
+ }
103
+ return suiteTags
104
+ }
105
+
73
106
  module.exports = class CiPlugin extends Plugin {
74
107
  constructor (...args) {
75
108
  super(...args)
76
109
 
77
110
  this.fileLineToProbeId = new Map()
78
111
  this.rootDir = process.cwd() // fallback in case :session:start events are not emitted
112
+ this._testSuiteSpansByTestSuite = new Map()
79
113
 
80
114
  this.addSub(`ci:${this.constructor.id}:library-configuration`, (ctx) => {
81
115
  const { onDone, isParallel, frameworkVersion } = ctx
@@ -265,6 +299,53 @@ module.exports = class CiPlugin extends Plugin {
265
299
  // TODO: Add telemetry for this type of error
266
300
  return onDone({ err: new Error('No modified tests could have been retrieved') })
267
301
  })
302
+
303
+ this.addSub(`ci:${this.constructor.id}:worker-report:trace`, traces => {
304
+ const formattedTraces = JSON.parse(traces)
305
+
306
+ for (const trace of formattedTraces) {
307
+ for (const span of trace) {
308
+ span.span_id = id(span.span_id)
309
+ span.trace_id = id(span.trace_id)
310
+ span.parent_id = id(span.parent_id)
311
+
312
+ if (span.name?.startsWith(`${this.constructor.id}.`)) {
313
+ // augment with git information (since it will not be available in the worker)
314
+ for (const key in this.testEnvironmentMetadata) {
315
+ // CAREFUL: this bypasses the metadata/metrics distinction
316
+ // Be careful not to pass numbers in `meta`
317
+ if (key.startsWith('git.')) {
318
+ span.meta[key] = this.testEnvironmentMetadata[key]
319
+ }
320
+ }
321
+ }
322
+
323
+ // Only test hooks run in the cucumber worker, so the test events do not have the
324
+ // test session, test module and test suite ids. We have to update them here.
325
+ if (span.name === 'cucumber.test' || span.name === 'mocha.test') {
326
+ const testSuite = span.meta[TEST_SUITE]
327
+ const testSuiteSpan = this._testSuiteSpansByTestSuite.get(testSuite)
328
+ if (!testSuiteSpan) {
329
+ log.warn(`Test suite span not found for test span with test suite ${testSuite}`)
330
+ continue
331
+ }
332
+
333
+ const testSuiteTags = getTestSuiteLevelVisibilityTags(testSuiteSpan, this.constructor.id)
334
+ span.meta = {
335
+ ...span.meta,
336
+ ...testSuiteTags
337
+ }
338
+ }
339
+ }
340
+ this.tracer._exporter.export(trace)
341
+ }
342
+ })
343
+
344
+ this.addSub(`ci:${this.constructor.id}:worker-report:logs`, (logsPayloads) => {
345
+ JSON.parse(logsPayloads).forEach(({ logMessage }) => {
346
+ this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, logMessage)
347
+ })
348
+ })
268
349
  }
269
350
 
270
351
  get telemetry () {
@@ -298,7 +379,20 @@ module.exports = class CiPlugin extends Plugin {
298
379
  this.di = getDiClient()
299
380
  }
300
381
 
301
- this.testEnvironmentMetadata = getTestEnvironmentMetadata(this.constructor.id, this.config)
382
+ if (this.testConfiguration) { // no need to recalculate as it's constant
383
+ return
384
+ }
385
+
386
+ const exporter = this.config.experimental?.exporter
387
+ const workerTestFramework = WORKER_EXPORTER_TO_TEST_FRAMEWORK[exporter]
388
+ this.shouldSkipGitMetadataExtraction = workerTestFramework &&
389
+ TEST_FRAMEWORKS_TO_SKIP_GIT_METADATA_EXTRACTION.has(workerTestFramework)
390
+
391
+ this.testEnvironmentMetadata = getTestEnvironmentMetadata(
392
+ this.constructor.id,
393
+ this.config,
394
+ this.shouldSkipGitMetadataExtraction
395
+ )
302
396
 
303
397
  const {
304
398
  [GIT_REPOSITORY_URL]: repositoryUrl,
@@ -318,9 +412,9 @@ module.exports = class CiPlugin extends Plugin {
318
412
  [GIT_COMMIT_HEAD_MESSAGE]: commitHeadMessage
319
413
  } = this.testEnvironmentMetadata
320
414
 
321
- this.repositoryRoot = repositoryRoot || process.cwd()
415
+ this.repositoryRoot = repositoryRoot || getRepositoryRoot() || process.cwd()
322
416
 
323
- this.codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot)
417
+ this.codeOwnersEntries = getCodeOwnersFileEntries(this.repositoryRoot)
324
418
 
325
419
  this.ciProviderName = ciProviderName
326
420
 
@@ -11,6 +11,9 @@ class CompositePlugin extends Plugin {
11
11
  }
12
12
  }
13
13
 
14
+ /**
15
+ * @override
16
+ */
14
17
  configure (config) {
15
18
  super.configure(config)
16
19
  for (const name in this.constructor.plugins) {
@@ -13,6 +13,7 @@ module.exports = {
13
13
  get '@google-cloud/vertexai' () { return require('../../../datadog-plugin-google-cloud-vertexai/src') },
14
14
  get '@grpc/grpc-js' () { return require('../../../datadog-plugin-grpc/src') },
15
15
  get '@hapi/hapi' () { return require('../../../datadog-plugin-hapi/src') },
16
+ get '@happy-dom/jest-environment' () { return require('../../../datadog-plugin-jest/src') },
16
17
  get '@jest/core' () { return require('../../../datadog-plugin-jest/src') },
17
18
  get '@jest/test-sequencer' () { return require('../../../datadog-plugin-jest/src') },
18
19
  get '@jest/transform' () { return require('../../../datadog-plugin-jest/src') },
@@ -73,6 +74,7 @@ module.exports = {
73
74
  get 'mocha-each' () { return require('../../../datadog-plugin-mocha/src') },
74
75
  get vitest () { return require('../../../datadog-plugin-vitest/src') },
75
76
  get workerpool () { return require('../../../datadog-plugin-mocha/src') },
77
+ get tinypool () { return require('../../../datadog-plugin-vitest/src') },
76
78
  get moleculer () { return require('../../../datadog-plugin-moleculer/src') },
77
79
  get mongodb () { return require('../../../datadog-plugin-mongodb-core/src') },
78
80
  get 'mongodb-core' () { return require('../../../datadog-plugin-mongodb-core/src') },
@@ -6,6 +6,24 @@ const dc = require('dc-polyfill')
6
6
  const logger = require('../log')
7
7
  const { storage } = require('../../../datadog-core')
8
8
 
9
+ /**
10
+ * Base class for all Datadog plugins.
11
+ *
12
+ * Subclasses MUST define a static field `id` with the integration identifier
13
+ * used across channels, span names, tags and telemetry.
14
+ *
15
+ * Example:
16
+ * ```js
17
+ * class MyPlugin extends Plugin {
18
+ * static id = 'myframework'
19
+ * }
20
+ * ```
21
+ *
22
+ * Notes about the tracer instance:
23
+ * - In some contexts the tracer may be wrapped and available as `{ _tracer: Tracer }`.
24
+ * Use the `tracer` getter which normalizes access.
25
+ */
26
+
9
27
  class Subscription {
10
28
  constructor (event, handler) {
11
29
  this._channel = dc.channel(event)
@@ -50,6 +68,12 @@ class StoreBinding {
50
68
  }
51
69
 
52
70
  module.exports = class Plugin {
71
+ /**
72
+ * Create a new plugin instance.
73
+ *
74
+ * @param {object} tracer Tracer instance or wrapper containing it under `_tracer`.
75
+ * @param {object} tracerConfig Global tracer configuration object.
76
+ */
53
77
  constructor (tracer, tracerConfig) {
54
78
  this._subscriptions = []
55
79
  this._bindings = []
@@ -59,10 +83,22 @@ module.exports = class Plugin {
59
83
  this._tracerConfig = tracerConfig // global tracer configuration
60
84
  }
61
85
 
86
+ /**
87
+ * Normalized tracer access. Returns the underlying tracer even if wrapped.
88
+ *
89
+ * @returns {object}
90
+ */
62
91
  get tracer () {
63
92
  return this._tracer?._tracer || this._tracer
64
93
  }
65
94
 
95
+ /**
96
+ * Enter a context with the provided span bound in storage.
97
+ *
98
+ * @param {object} span The span to bind as current.
99
+ * @param {object=} store Optional existing store to extend; if omitted, uses current store.
100
+ * @returns {void}
101
+ */
66
102
  enter (span, store) {
67
103
  store = store || storage('legacy').getStore()
68
104
  storage('legacy').enterWith({ ...store, span })
@@ -74,8 +110,19 @@ module.exports = class Plugin {
74
110
  storage('legacy').enterWith({ noop: true })
75
111
  }
76
112
 
113
+ /**
114
+ * Subscribe to a diagnostic channel with automatic error handling and enable/disable lifecycle.
115
+ *
116
+ * @param {string} channelName Diagnostic channel name.
117
+ * @param {(...args: unknown[]) => unknown} handler Handler invoked on messages.
118
+ * @returns {void}
119
+ */
77
120
  addSub (channelName, handler) {
78
121
  const plugin = this
122
+ /**
123
+ * @this {unknown}
124
+ * @returns {unknown}
125
+ */
79
126
  const wrappedHandler = function () {
80
127
  try {
81
128
  return handler.apply(this, arguments)
@@ -88,10 +135,23 @@ module.exports = class Plugin {
88
135
  this._subscriptions.push(new Subscription(channelName, wrappedHandler))
89
136
  }
90
137
 
138
+ /**
139
+ * Bind the tracer store to a diagnostic channel with a transform function.
140
+ *
141
+ * @param {string} channelName Diagnostic channel name.
142
+ * @param {(data: unknown) => object} transform Transform to compute the bound store.
143
+ * @returns {void}
144
+ */
91
145
  addBind (channelName, transform) {
92
146
  this._bindings.push(new StoreBinding(channelName, transform))
93
147
  }
94
148
 
149
+ /**
150
+ * Attach an error to the current active span (if any).
151
+ *
152
+ * @param {unknown} error Error object or sentinel value.
153
+ * @returns {void}
154
+ */
95
155
  addError (error) {
96
156
  const store = storage('legacy').getStore()
97
157
 
@@ -102,6 +162,13 @@ module.exports = class Plugin {
102
162
  }
103
163
  }
104
164
 
165
+ /**
166
+ * Enable or disable the plugin and (re)apply its configuration.
167
+ *
168
+ * @param {boolean|object} config Either a boolean to enable/disable or a configuration object
169
+ * containing at least `{ enabled: boolean }`.
170
+ * @returns {void}
171
+ */
105
172
  configure (config) {
106
173
  if (typeof config === 'boolean') {
107
174
  config = { enabled: config }
@@ -0,0 +1,129 @@
1
+ 'use strict'
2
+
3
+ const os = require('os')
4
+ const path = require('path')
5
+ const fs = require('fs')
6
+ const crypto = require('crypto')
7
+ const cp = require('child_process')
8
+
9
+ const log = require('../../log')
10
+ const { getEnvironmentVariable } = require('../../config-helper')
11
+ const { isTrue } = require('../../util')
12
+
13
+ let isGitEnabled = isTrue(getEnvironmentVariable('DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_ENABLED'))
14
+ const GIT_CACHE_DIR = getEnvironmentVariable('DD_EXPERIMENTAL_TEST_OPT_GIT_CACHE_DIR') ||
15
+ path.join(os.tmpdir(), 'dd-trace-git-cache')
16
+
17
+ function ensureCacheDir () {
18
+ if (!isGitEnabled) return false
19
+
20
+ try {
21
+ if (fs.existsSync(GIT_CACHE_DIR)) {
22
+ const stats = fs.statSync(GIT_CACHE_DIR)
23
+ if (!stats.isDirectory()) {
24
+ throw new Error(`Cache directory path exists but is not a directory: ${GIT_CACHE_DIR}`)
25
+ }
26
+ } else {
27
+ fs.mkdirSync(GIT_CACHE_DIR, { recursive: true })
28
+ }
29
+ return true
30
+ } catch (err) {
31
+ log.error('Failed to create git cache directory, disabling cache', err)
32
+ isGitEnabled = false
33
+ return false
34
+ }
35
+ }
36
+
37
+ // Initialize cache directory at module load time
38
+ ensureCacheDir()
39
+
40
+ function getCacheKey (cmd, flags) {
41
+ // Create a hash of the command and flags to use as cache key
42
+ const commandString = `${cmd} ${flags.join(' ')}`
43
+ return crypto.createHash('sha256').update(commandString).digest('hex')
44
+ }
45
+
46
+ function getCacheFilePath (cacheKey) {
47
+ return path.join(GIT_CACHE_DIR, `${cacheKey}.cache`)
48
+ }
49
+
50
+ function getCache (cacheKey) {
51
+ if (!isGitEnabled) return null
52
+
53
+ try {
54
+ const cacheFilePath = getCacheFilePath(cacheKey)
55
+ if (!fs.existsSync(cacheFilePath)) {
56
+ return null
57
+ }
58
+
59
+ const content = fs.readFileSync(cacheFilePath, 'utf8')
60
+ return content
61
+ } catch (err) {
62
+ log.error('Failed to read git cache', err)
63
+ return null
64
+ }
65
+ }
66
+
67
+ function setCache (cacheKey, result) {
68
+ if (!isGitEnabled) return
69
+
70
+ // Ensure cache directory exists
71
+ if (!ensureCacheDir()) return
72
+
73
+ try {
74
+ const cacheFilePath = getCacheFilePath(cacheKey)
75
+ fs.writeFileSync(cacheFilePath, result, 'utf8')
76
+ } catch (err) {
77
+ log.error('Failed to write git cache', err)
78
+ }
79
+ }
80
+
81
+ function cachedExec (cmd, flags, options) {
82
+ if (options === undefined) {
83
+ options = { stdio: 'pipe' }
84
+ }
85
+ if (!isGitEnabled) {
86
+ return cp.execFileSync(cmd, flags, options)
87
+ }
88
+ const cacheKey = getCacheKey(cmd, flags)
89
+ const cachedResult = getCache(cacheKey)
90
+ if (cachedResult !== null) {
91
+ if (cachedResult.startsWith('__GIT_COMMAND_FAILED__')) {
92
+ let error
93
+ try {
94
+ const errorData = cachedResult.replace('__GIT_COMMAND_FAILED__', '')
95
+ const { message, code, status, errno } = JSON.parse(errorData)
96
+ error = new Error(message)
97
+ error.code = code
98
+ error.status = status
99
+ error.errno = errno
100
+ } catch {
101
+ // we couldn't parse the error data, so we'll throw a generic error
102
+ throw new Error('Git command failed')
103
+ }
104
+ throw error
105
+ }
106
+ return cachedResult
107
+ }
108
+ try {
109
+ const result = cp.execFileSync(cmd, flags, options)
110
+ setCache(cacheKey, result)
111
+ return result
112
+ } catch (err) {
113
+ const cacheValue = '__GIT_COMMAND_FAILED__' +
114
+ JSON.stringify({
115
+ code: err.code,
116
+ status: err.status,
117
+ errno: err.errno,
118
+ message: err.message
119
+ })
120
+ setCache(cacheKey, cacheValue)
121
+ throw err
122
+ }
123
+ }
124
+
125
+ module.exports = {
126
+ getCacheKey,
127
+ getCacheFilePath,
128
+ cachedExec
129
+ }
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const cp = require('child_process')
4
3
  const os = require('os')
5
4
  const path = require('path')
6
5
  const fs = require('fs')
@@ -36,6 +35,7 @@ const {
36
35
  } = require('../../ci-visibility/telemetry')
37
36
  const { filterSensitiveInfoFromRepository } = require('./url')
38
37
  const { storage } = require('../../../../datadog-core')
38
+ const { cachedExec } = require('./git-cache')
39
39
 
40
40
  const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB
41
41
 
@@ -58,10 +58,12 @@ function sanitizedExec (
58
58
  startTime = Date.now()
59
59
  }
60
60
  try {
61
- let result = cp.execFileSync(cmd, flags, { stdio: 'pipe' }).toString()
61
+ let result = cachedExec(cmd, flags, { stdio: 'pipe' }).toString()
62
+
62
63
  if (shouldTrim) {
63
64
  result = result.replaceAll(/(\r\n|\n|\r)/gm, '')
64
65
  }
66
+
65
67
  if (durationMetric) {
66
68
  distributionMetric(durationMetric.name, durationMetric.tags, Date.now() - startTime)
67
69
  }
@@ -94,7 +96,7 @@ function isGitAvailable () {
94
96
  const isWindows = os.platform() === 'win32'
95
97
  const command = isWindows ? 'where' : 'which'
96
98
  try {
97
- cp.execFileSync(command, ['git'], { stdio: 'pipe' })
99
+ cachedExec(command, ['git'])
98
100
  return true
99
101
  } catch {
100
102
  incrementCountMetric(TELEMETRY_GIT_COMMAND_ERRORS, { command: 'check_git', exitCode: 'missing' })
@@ -114,7 +116,7 @@ function isShallowRepository () {
114
116
 
115
117
  function getGitVersion () {
116
118
  const gitVersionString = sanitizedExec('git', ['version'])
117
- const gitVersionMatches = gitVersionString.match(/git version (\d+)\.(\d+)\.(\d+)/)
119
+ const gitVersionMatches = /** @type {RegExpMatchArray} */ (gitVersionString.match(/git version (\d+)\.(\d+)\.(\d+)/))
118
120
  try {
119
121
  return {
120
122
  major: Number.parseInt(gitVersionMatches[1]),
@@ -150,27 +152,32 @@ function unshallowRepository (parentOnly = false) {
150
152
 
151
153
  incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'unshallow' })
152
154
  const start = Date.now()
155
+ let flags = [
156
+ ...baseGitOptions,
157
+ revParseHead
158
+ ]
153
159
  try {
154
- cp.execFileSync('git', [
155
- ...baseGitOptions,
156
- revParseHead
157
- ], { stdio: 'pipe' })
160
+ cachedExec('git', flags)
158
161
  } catch (err) {
159
162
  // If the local HEAD is a commit that has not been pushed to the remote, the above command will fail.
160
- log.error('Git plugin error executing git command', err)
163
+ log.warn(`Git unshallow failed: ${flags.join(' ')}`)
161
164
  incrementCountMetric(
162
165
  TELEMETRY_GIT_COMMAND_ERRORS,
163
166
  { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
164
167
  )
165
- const upstreamRemote = sanitizedExec('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
168
+ const upstreamRemote = sanitizedExec(
169
+ 'git',
170
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']
171
+ )
172
+ flags = [
173
+ ...baseGitOptions,
174
+ upstreamRemote
175
+ ]
166
176
  try {
167
- cp.execFileSync('git', [
168
- ...baseGitOptions,
169
- upstreamRemote
170
- ], { stdio: 'pipe' })
177
+ cachedExec('git', flags)
171
178
  } catch (err) {
172
- // If the CI is working on a detached HEAD or branch tracking hasnt been set up, the above command will fail.
173
- log.error('Git plugin error executing fallback git command', err)
179
+ // If the CI is working on a detached HEAD or branch tracking hasn't been set up, the above command will fail.
180
+ log.warn(`Git unshallow failed again: ${flags.join(' ')}`)
174
181
  incrementCountMetric(
175
182
  TELEMETRY_GIT_COMMAND_ERRORS,
176
183
  { command: 'unshallow', errorType: err.code, exitCode: err.status || err.errno }
@@ -202,7 +209,7 @@ function getLatestCommits () {
202
209
  incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'get_local_commits' })
203
210
  const startTime = Date.now()
204
211
  try {
205
- const result = cp.execFileSync('git', ['log', '--format=%H', '-n 1000', '--since="1 month ago"'], { stdio: 'pipe' })
212
+ const result = cachedExec('git', ['log', '--format=%H', '-n 1000', '--since="1 month ago"'])
206
213
  .toString()
207
214
  .split('\n')
208
215
  .filter(Boolean)
@@ -272,10 +279,9 @@ function checkAndFetchBranch (branch, remoteName) {
272
279
  try {
273
280
  // `git show-ref --verify --quiet refs/remotes/${remoteName}/${branch}` will exit 0 if the branch exists
274
281
  // Otherwise it will exit 1
275
- cp.execFileSync(
282
+ cachedExec(
276
283
  'git',
277
284
  ['show-ref', '--verify', '--quiet', `refs/remotes/${remoteName}/${branch}`],
278
- { stdio: 'pipe' }
279
285
  )
280
286
  // branch exists locally, so we finish
281
287
  } catch {
@@ -285,22 +291,22 @@ function checkAndFetchBranch (branch, remoteName) {
285
291
  // `git ls-remote --heads origin my-branch` will exit 0 even if the branch doesn't exist.
286
292
  // The piece of information we need is whether the command outputs anything.
287
293
  // `git ls-remote --heads origin my-branch` could exit an error code if the remote does not exist.
288
- const remoteHeads = cp.execFileSync(
294
+ const remoteHeads = cachedExec(
289
295
  'git',
290
296
  ['ls-remote', '--heads', remoteName, branch],
291
297
  { stdio: 'pipe', timeout: 2000 }
292
298
  )
293
299
  if (remoteHeads) {
294
300
  // branch exists, so we'll fetch it
295
- cp.execFileSync(
301
+ cachedExec(
296
302
  'git',
297
303
  ['fetch', '--depth', '1', remoteName, branch],
298
304
  { stdio: 'pipe', timeout: 5000 }
299
305
  )
300
306
  }
301
- } catch (e) {
307
+ } catch (err) {
302
308
  // branch does not exist or couldn't be fetched, so we can't do anything
303
- log.error('Git plugin error checking and fetching branch', e)
309
+ log.debug('Git plugin error checking and fetching branch', err)
304
310
  }
305
311
  }
306
312
  }
@@ -358,7 +364,7 @@ function getCommitsRevList (commitsToExclude, commitsToInclude) {
358
364
  incrementCountMetric(TELEMETRY_GIT_COMMAND, { command: 'get_objects' })
359
365
  const startTime = Date.now()
360
366
  try {
361
- result = cp.execFileSync(
367
+ result = cachedExec(
362
368
  'git',
363
369
  [
364
370
  'rev-list',
@@ -403,7 +409,7 @@ function generatePackFilesForCommits (commitsToUpload) {
403
409
  // Generates pack files to upload and
404
410
  // returns the ordered list of packfiles' paths
405
411
  function execGitPackObjects (targetPath) {
406
- return cp.execFileSync(
412
+ return cachedExec(
407
413
  'git',
408
414
  [
409
415
  'pack-objects',
@@ -450,6 +456,13 @@ function generatePackFilesForCommits (commitsToUpload) {
450
456
  return result
451
457
  }
452
458
 
459
+ function getRepositoryRoot () {
460
+ return sanitizedExec(
461
+ 'git',
462
+ ['rev-parse', '--show-toplevel']
463
+ )
464
+ }
465
+
453
466
  // If there is ciMetadata, it takes precedence.
454
467
  function getGitMetadata (ciMetadata) {
455
468
  const {
@@ -480,7 +493,7 @@ function getGitMetadata (ciMetadata) {
480
493
  commitMessage || sanitizedExec('git', ['show', '-s', '--format=%B'], null, null, null, false),
481
494
  [GIT_BRANCH]: branch || sanitizedExec('git', ['rev-parse', '--abbrev-ref', 'HEAD']),
482
495
  [GIT_COMMIT_SHA]: commitSHA || sanitizedExec('git', ['rev-parse', 'HEAD']),
483
- [CI_WORKSPACE_PATH]: ciWorkspacePath || sanitizedExec('git', ['rev-parse', '--show-toplevel']),
496
+ [CI_WORKSPACE_PATH]: ciWorkspacePath || getRepositoryRoot(),
484
497
  }
485
498
 
486
499
  if (headCommitSha) {
@@ -596,5 +609,6 @@ module.exports = {
596
609
  getLocalBranches,
597
610
  getMergeBase,
598
611
  getCounts,
599
- fetchHeadCommitSha
612
+ fetchHeadCommitSha,
613
+ getRepositoryRoot
600
614
  }