dd-trace 5.73.0 → 5.75.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 (37) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +39 -7
  3. package/loader-hook.mjs +52 -1
  4. package/package.json +8 -16
  5. package/packages/datadog-core/src/utils/src/set.js +5 -1
  6. package/packages/datadog-esbuild/index.js +105 -36
  7. package/packages/datadog-esbuild/src/utils.js +198 -0
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +0 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +2 -2
  10. package/packages/datadog-instrumentations/src/express.js +82 -0
  11. package/packages/datadog-instrumentations/src/helpers/router-helper.js +238 -0
  12. package/packages/datadog-instrumentations/src/jest.js +2 -1
  13. package/packages/datadog-instrumentations/src/mariadb.js +9 -7
  14. package/packages/datadog-instrumentations/src/playwright.js +226 -93
  15. package/packages/datadog-instrumentations/src/router.js +63 -6
  16. package/packages/datadog-instrumentations/src/vitest.js +44 -12
  17. package/packages/datadog-instrumentations/src/ws.js +3 -3
  18. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -1
  19. package/packages/datadog-plugin-express/src/code_origin.js +2 -0
  20. package/packages/datadog-plugin-playwright/src/index.js +74 -31
  21. package/packages/datadog-plugin-ws/src/close.js +1 -1
  22. package/packages/datadog-shimmer/src/shimmer.js +2 -0
  23. package/packages/dd-trace/src/aiguard/sdk.js +25 -3
  24. package/packages/dd-trace/src/aiguard/tags.js +4 -1
  25. package/packages/dd-trace/src/config-helper.js +4 -1
  26. package/packages/dd-trace/src/config.js +599 -592
  27. package/packages/dd-trace/src/config_defaults.js +14 -12
  28. package/packages/dd-trace/src/plugins/util/ci.js +3 -2
  29. package/packages/dd-trace/src/plugins/util/stacktrace.js +16 -1
  30. package/packages/dd-trace/src/proxy.js +1 -1
  31. package/packages/dd-trace/src/supported-configurations.json +1 -0
  32. package/packages/dd-trace/src/telemetry/endpoints.js +27 -1
  33. package/packages/dd-trace/src/telemetry/index.js +16 -13
  34. package/packages/dd-trace/src/telemetry/logs/log-collector.js +5 -3
  35. package/register.js +1 -11
  36. package/scripts/preinstall.js +3 -1
  37. package/version.js +2 -1
@@ -153,6 +153,10 @@ function isCliApiPackage (vitestPackage) {
153
153
  return vitestPackage.s?.name === 'startVitest'
154
154
  }
155
155
 
156
+ function isTestPackage (testPackage) {
157
+ return testPackage.V?.name === 'VitestTestRunner'
158
+ }
159
+
156
160
  function getSessionStatus (state) {
157
161
  if (state.getCountOfFailedTests() > 0) {
158
162
  return 'fail'
@@ -240,7 +244,9 @@ function getSortWrapper (sort, frameworkVersion) {
240
244
  if (isFlakyTestRetriesEnabled && !this.ctx.config.retry && flakyTestRetriesCount > 0) {
241
245
  this.ctx.config.retry = flakyTestRetriesCount
242
246
  try {
243
- const workspaceProject = this.ctx.getCoreWorkspaceProject()
247
+ const workspaceProject = this.ctx.getCoreWorkspaceProject
248
+ ? this.ctx.getCoreWorkspaceProject()
249
+ : this.ctx.getRootProject()
244
250
  workspaceProject._provided._ddIsFlakyTestRetriesEnabled = isFlakyTestRetriesEnabled
245
251
  } catch {
246
252
  log.warn('Could not send library configuration to workers.')
@@ -272,7 +278,9 @@ function getSortWrapper (sort, frameworkVersion) {
272
278
  // TODO: use this to pass session and module IDs to the worker, instead of polluting process.env
273
279
  // Note: setting this.ctx.config.provide directly does not work because it's cached
274
280
  try {
275
- const workspaceProject = this.ctx.getCoreWorkspaceProject()
281
+ const workspaceProject = this.ctx.getCoreWorkspaceProject
282
+ ? this.ctx.getCoreWorkspaceProject()
283
+ : this.ctx.getRootProject()
276
284
  workspaceProject._provided._ddIsKnownTestsEnabled = isKnownTestsEnabled
277
285
  workspaceProject._provided._ddKnownTests = knownTests
278
286
  workspaceProject._provided._ddIsEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
@@ -290,7 +298,9 @@ function getSortWrapper (sort, frameworkVersion) {
290
298
 
291
299
  if (isDiEnabled) {
292
300
  try {
293
- const workspaceProject = this.ctx.getCoreWorkspaceProject()
301
+ const workspaceProject = this.ctx.getCoreWorkspaceProject
302
+ ? this.ctx.getCoreWorkspaceProject()
303
+ : this.ctx.getRootProject()
294
304
  workspaceProject._provided._ddIsDiEnabled = isDiEnabled
295
305
  } catch {
296
306
  log.warn('Could not send Dynamic Instrumentation configuration to workers.')
@@ -305,7 +315,9 @@ function getSortWrapper (sort, frameworkVersion) {
305
315
  } else {
306
316
  const testManagementTests = receivedTestManagementTests
307
317
  try {
308
- const workspaceProject = this.ctx.getCoreWorkspaceProject()
318
+ const workspaceProject = this.ctx.getCoreWorkspaceProject
319
+ ? this.ctx.getCoreWorkspaceProject()
320
+ : this.ctx.getRootProject()
309
321
  workspaceProject._provided._ddIsTestManagementTestsEnabled = isTestManagementTestsEnabled
310
322
  workspaceProject._provided._ddTestManagementAttemptToFixRetries = testManagementAttemptToFixRetries
311
323
  workspaceProject._provided._ddTestManagementTests = testManagementTests
@@ -321,7 +333,9 @@ function getSortWrapper (sort, frameworkVersion) {
321
333
  log.error('Could not get modified tests.')
322
334
  } else {
323
335
  try {
324
- const workspaceProject = this.ctx.getCoreWorkspaceProject()
336
+ const workspaceProject = this.ctx.getCoreWorkspaceProject
337
+ ? this.ctx.getCoreWorkspaceProject()
338
+ : this.ctx.getRootProject()
325
339
  workspaceProject._provided._ddIsImpactedTestsEnabled = isImpactedTestsEnabled
326
340
  workspaceProject._provided._ddModifiedFiles = modifiedFiles
327
341
  } catch {
@@ -443,13 +457,7 @@ function getStartVitestWrapper (cliApiPackage, frameworkVersion) {
443
457
  return cliApiPackage
444
458
  }
445
459
 
446
- addHook({
447
- name: 'vitest',
448
- versions: ['>=1.6.0'],
449
- file: 'dist/runners.js'
450
- }, (vitestPackage) => {
451
- const { VitestTestRunner } = vitestPackage
452
-
460
+ function wrapVitestTestRunner (VitestTestRunner) {
453
461
  // `onBeforeRunTask` is run before any repetition or attempt is run
454
462
  // `onBeforeRunTask` is an async function
455
463
  shimmer.wrap(VitestTestRunner.prototype, 'onBeforeRunTask', onBeforeRunTask => function (task) {
@@ -744,6 +752,30 @@ addHook({
744
752
 
745
753
  return result
746
754
  })
755
+ }
756
+
757
+ addHook({
758
+ name: 'vitest',
759
+ versions: ['>=4.0.0'],
760
+ filePattern: 'dist/chunks/test.*'
761
+ }, (testPackage) => {
762
+ if (!isTestPackage(testPackage)) {
763
+ return testPackage
764
+ }
765
+
766
+ wrapVitestTestRunner(testPackage.V)
767
+
768
+ return testPackage
769
+ })
770
+
771
+ addHook({
772
+ name: 'vitest',
773
+ versions: ['>=1.6.0 <4.0.0'],
774
+ file: 'dist/runners.js'
775
+ }, (vitestPackage) => {
776
+ const { VitestTestRunner } = vitestPackage
777
+
778
+ wrapVitestTestRunner(VitestTestRunner)
747
779
 
748
780
  return vitestPackage
749
781
  })
@@ -41,7 +41,7 @@ function wrapSend (send) {
41
41
 
42
42
  const [data, options, cb] = arguments
43
43
 
44
- const ctx = { data, socket: this._sender._socket }
44
+ const ctx = { data, socket: this._sender?._socket }
45
45
 
46
46
  return typeof cb === 'function'
47
47
  ? producerCh.traceCallback(send, undefined, ctx, this, data, options, cb)
@@ -70,7 +70,7 @@ function createWrappedHandler (handler) {
70
70
  return function wrappedMessageHandler (data, binary) {
71
71
  const byteLength = dataLength(data)
72
72
 
73
- const ctx = { data, binary, socket: this._sender._socket, byteLength }
73
+ const ctx = { data, binary, socket: this._sender?._socket, byteLength }
74
74
 
75
75
  return receiverCh.traceSync(handler, ctx, this, data, binary)
76
76
  }
@@ -93,7 +93,7 @@ function wrapClose (close) {
93
93
  // if both are true then the self is sending the close event
94
94
  const isPeerClose = this._closeFrameReceived === true && this._closeFrameSent === false
95
95
 
96
- const ctx = { code, data, socket: this._sender._socket, isPeerClose }
96
+ const ctx = { code, data, socket: this._sender?._socket, isPeerClose }
97
97
 
98
98
  return closeCh.traceSync(close, ctx, this, ...arguments)
99
99
  }
@@ -282,7 +282,6 @@ function normalizeConfig (config, serviceIdentifier) {
282
282
  return {
283
283
  ...config,
284
284
  ...specificConfig,
285
- splitByAwsService: config.splitByAwsService !== false,
286
285
  batchPropagationEnabled,
287
286
  hooks
288
287
  }
@@ -19,6 +19,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
19
19
  })
20
20
 
21
21
  this.addSub('apm:express:route:added', ({ topOfStackFunc, layer }) => {
22
+ if (!layer) return
22
23
  if (layerTags.has(layer)) return
23
24
  layerTags.set(layer, entryTags(topOfStackFunc))
24
25
  })
@@ -30,6 +31,7 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
30
31
  })
31
32
 
32
33
  this.addSub('apm:router:route:added', ({ topOfStackFunc, layer }) => {
34
+ if (!layer) return
33
35
  if (layerTags.has(layer)) return
34
36
  layerTags.set(layer, entryTags(topOfStackFunc))
35
37
  })
@@ -47,6 +47,7 @@ const {
47
47
  TELEMETRY_EVENT_FINISHED
48
48
  } = require('../../dd-trace/src/ci-visibility/telemetry')
49
49
  const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry')
50
+ const log = require('../../dd-trace/src/log')
50
51
 
51
52
  class PlaywrightPlugin extends CiPlugin {
52
53
  static id = 'playwright'
@@ -174,7 +175,10 @@ class PlaywrightPlugin extends CiPlugin {
174
175
  }) => {
175
176
  const store = storage('legacy').getStore()
176
177
  const span = store && store.span
177
- if (!span) return
178
+ if (!span) {
179
+ log.error('ci:playwright:test:page-goto: test span not found')
180
+ return
181
+ }
178
182
 
179
183
  if (isRumActive) {
180
184
  span.setTag(TEST_IS_RUM_ACTIVE, 'true')
@@ -198,36 +202,6 @@ class PlaywrightPlugin extends CiPlugin {
198
202
  }
199
203
  })
200
204
 
201
- this.addBind('ci:playwright:test:start', (ctx) => {
202
- const {
203
- testName,
204
- testSuiteAbsolutePath,
205
- testSourceLine,
206
- browserName,
207
- isDisabled
208
- } = ctx
209
- const store = storage('legacy').getStore()
210
- const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
211
- const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
212
- const span = this.startTestSpan(
213
- testName,
214
- testSuiteAbsolutePath,
215
- testSuite,
216
- testSourceFile,
217
- testSourceLine,
218
- browserName
219
- )
220
-
221
- if (isDisabled) {
222
- span.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
223
- }
224
-
225
- ctx.parentStore = store
226
- ctx.currentStore = { ...store, span }
227
-
228
- return ctx.currentStore
229
- })
230
-
231
205
  this.addSub('ci:playwright:worker:report', (serializedTraces) => {
232
206
  const traces = JSON.parse(serializedTraces)
233
207
  const formattedTraces = []
@@ -277,6 +251,36 @@ class PlaywrightPlugin extends CiPlugin {
277
251
  })
278
252
  })
279
253
 
254
+ this.addBind('ci:playwright:test:start', (ctx) => {
255
+ const {
256
+ testName,
257
+ testSuiteAbsolutePath,
258
+ testSourceLine,
259
+ browserName,
260
+ isDisabled
261
+ } = ctx
262
+ const store = storage('legacy').getStore()
263
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
264
+ const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
265
+ const span = this.startTestSpan(
266
+ testName,
267
+ testSuiteAbsolutePath,
268
+ testSuite,
269
+ testSourceFile,
270
+ testSourceLine,
271
+ browserName
272
+ )
273
+
274
+ if (isDisabled) {
275
+ span.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
276
+ }
277
+
278
+ ctx.parentStore = store
279
+ ctx.currentStore = { ...store, span }
280
+
281
+ return ctx.currentStore
282
+ })
283
+
280
284
  this.addSub('ci:playwright:test:finish', ({
281
285
  span,
282
286
  testStatus,
@@ -393,6 +397,45 @@ class PlaywrightPlugin extends CiPlugin {
393
397
  this.tracer._exporter.flush(onDone)
394
398
  }
395
399
  })
400
+
401
+ this.addSub('ci:playwright:test:skip', ({
402
+ testName,
403
+ testSuiteAbsolutePath,
404
+ testSourceLine,
405
+ browserName,
406
+ isNew,
407
+ isDisabled,
408
+ isModified,
409
+ isQuarantined
410
+ }) => {
411
+ const testSuite = getTestSuitePath(testSuiteAbsolutePath, this.rootDir)
412
+ const testSourceFile = getTestSuitePath(testSuiteAbsolutePath, this.repositoryRoot)
413
+ const span = this.startTestSpan(
414
+ testName,
415
+ testSuiteAbsolutePath,
416
+ testSuite,
417
+ testSourceFile,
418
+ testSourceLine,
419
+ browserName
420
+ )
421
+
422
+ span.setTag(TEST_STATUS, 'skip')
423
+
424
+ if (isNew) {
425
+ span.setTag(TEST_IS_NEW, 'true')
426
+ }
427
+ if (isDisabled) {
428
+ span.setTag(TEST_MANAGEMENT_IS_DISABLED, 'true')
429
+ }
430
+ if (isModified) {
431
+ span.setTag(TEST_IS_MODIFIED, 'true')
432
+ }
433
+ if (isQuarantined) {
434
+ span.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
435
+ }
436
+
437
+ span.finish()
438
+ })
396
439
  }
397
440
 
398
441
  // TODO: this runs both in worker and main process (main process: skipped tests that do not go through _runTest)
@@ -17,7 +17,7 @@ class WSClosePlugin extends TracingPlugin {
17
17
  if (!traceWebsocketMessagesEnabled) return
18
18
 
19
19
  const { code, data, socket, isPeerClose } = ctx
20
- if (!socket.spanContext) return
20
+ if (!socket?.spanContext) return
21
21
 
22
22
  const spanKind = isPeerClose ? 'consumer' : 'producer'
23
23
  const spanTags = socket.spanContext.spanTags
@@ -73,6 +73,8 @@ function copyObjectProperties (original, wrapped, skipKey) {
73
73
  * @returns {Function} The wrapped function.
74
74
  */
75
75
  function wrapFunction (original, wrapper) {
76
+ if (typeof original !== 'function') return original
77
+
76
78
  const wrapped = wrapper(original)
77
79
 
78
80
  if (typeof original === 'function') {
@@ -9,9 +9,15 @@ const {
9
9
  AI_GUARD_ACTION_TAG_KEY,
10
10
  AI_GUARD_BLOCKED_TAG_KEY,
11
11
  AI_GUARD_META_STRUCT_KEY,
12
- AI_GUARD_TOOL_NAME_TAG_KEY
12
+ AI_GUARD_TOOL_NAME_TAG_KEY,
13
+ AI_GUARD_TELEMETRY_REQUESTS,
14
+ AI_GUARD_TELEMETRY_TRUNCATED
13
15
  } = require('./tags')
14
16
  const log = require('../log')
17
+ const telemetryMetrics = require('../telemetry/metrics')
18
+ const tracerVersion = require('../../../../package.json').version
19
+
20
+ const appsecMetrics = telemetryMetrics.manager.namespace('appsec')
15
21
 
16
22
  const ALLOW = 'ALLOW'
17
23
 
@@ -58,6 +64,9 @@ class AIGuard extends NoopAIGuard {
58
64
  this.#headers = {
59
65
  'DD-API-KEY': config.apiKey,
60
66
  'DD-APPLICATION-KEY': config.appKey,
67
+ 'DD-AI-GUARD-VERSION': tracerVersion,
68
+ 'DD-AI-GUARD-SOURCE': 'SDK',
69
+ 'DD-AI-GUARD-LANGUAGE': 'nodejs'
61
70
  }
62
71
  const endpoint = config.experimental.aiguard.endpoint || `https://app.${config.site}/api/v2/ai-guard`
63
72
  this.#evaluateUrl = `${endpoint}/evaluate`
@@ -70,14 +79,22 @@ class AIGuard extends NoopAIGuard {
70
79
 
71
80
  #truncate (messages) {
72
81
  const size = Math.min(messages.length, this.#maxMessagesLength)
82
+ if (messages.length > size) {
83
+ appsecMetrics.count(AI_GUARD_TELEMETRY_TRUNCATED, { type: 'messages' }).inc(1)
84
+ }
73
85
  const result = messages.slice(-size)
74
86
 
87
+ let contentTruncated = false
75
88
  for (let i = 0; i < size; i++) {
76
89
  const message = result[i]
77
90
  if (message.content?.length > this.#maxContentSize) {
91
+ contentTruncated = true
78
92
  result[i] = { ...message, content: message.content.slice(0, this.#maxContentSize) }
79
93
  }
80
94
  }
95
+ if (contentTruncated) {
96
+ appsecMetrics.count(AI_GUARD_TELEMETRY_TRUNCATED, { type: 'content' }).inc(1)
97
+ }
81
98
  return result
82
99
  }
83
100
 
@@ -140,9 +157,11 @@ class AIGuard extends NoopAIGuard {
140
157
  payload,
141
158
  { url: this.#evaluateUrl, headers: this.#headers, timeout: this.#timeout })
142
159
  } catch (e) {
143
- throw new AIGuardClientError('Unexpected error calling AI Guard service', { cause: e })
160
+ appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1)
161
+ throw new AIGuardClientError(`Unexpected error calling AI Guard service: ${e.message}`, { cause: e })
144
162
  }
145
163
  if (response.status !== 200) {
164
+ appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1)
146
165
  throw new AIGuardClientError(
147
166
  `AI Guard service call failed, status ${response.status}`,
148
167
  { errors: response.body?.errors })
@@ -157,11 +176,14 @@ class AIGuard extends NoopAIGuard {
157
176
  reason = attr.reason
158
177
  blockingEnabled = attr.is_blocking_enabled ?? false
159
178
  } catch (e) {
179
+ appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { error: true }).inc(1)
160
180
  throw new AIGuardClientError(`AI Guard service returned unexpected response : ${response.body}`, { cause: e })
161
181
  }
182
+ const shouldBlock = block && blockingEnabled && action !== ALLOW
183
+ appsecMetrics.count(AI_GUARD_TELEMETRY_REQUESTS, { action, error: false, block: shouldBlock }).inc(1)
162
184
  span.setTag(AI_GUARD_ACTION_TAG_KEY, action)
163
185
  span.setTag(AI_GUARD_REASON_TAG_KEY, reason)
164
- if (block && blockingEnabled && action !== ALLOW) {
186
+ if (shouldBlock) {
165
187
  span.setTag(AI_GUARD_BLOCKED_TAG_KEY, 'true')
166
188
  throw new AIGuardAbortError(reason)
167
189
  }
@@ -7,5 +7,8 @@ module.exports = {
7
7
  AI_GUARD_ACTION_TAG_KEY: 'ai_guard.action',
8
8
  AI_GUARD_REASON_TAG_KEY: 'ai_guard.reason',
9
9
  AI_GUARD_BLOCKED_TAG_KEY: 'ai_guard.blocked',
10
- AI_GUARD_META_STRUCT_KEY: 'ai_guard'
10
+ AI_GUARD_META_STRUCT_KEY: 'ai_guard',
11
+
12
+ AI_GUARD_TELEMETRY_REQUESTS: 'ai_guard.requests',
13
+ AI_GUARD_TELEMETRY_TRUNCATED: 'ai_guard.truncated'
11
14
  }
@@ -37,7 +37,9 @@ for (const deprecation of Object.keys(deprecations)) {
37
37
  module.exports = {
38
38
  /**
39
39
  * Returns the environment variables that are supported by the tracer
40
- * (including all non-Datadog/OTEL specific environment variables)
40
+ * (including all non-Datadog/OTEL specific environment variables).
41
+ *
42
+ * This should only be called once in config.js to avoid copying the object frequently.
41
43
  *
42
44
  * @returns {TracerEnv} The environment variables
43
45
  */
@@ -78,6 +80,7 @@ module.exports = {
78
80
  * @returns {string|undefined}
79
81
  * @throws {Error} if the configuration is not supported
80
82
  */
83
+ // This method, and callers of this method, need to be updated to check for declarative config sources as well.
81
84
  getEnvironmentVariable (name) {
82
85
  if ((name.startsWith('DD_') || name.startsWith('OTEL_') || aliasToCanonical[name]) &&
83
86
  !supportedConfigurations[name]) {