dd-trace 5.63.2 → 5.64.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.
package/index.d.ts CHANGED
@@ -712,6 +712,16 @@ declare namespace tracer {
712
712
  * @default true
713
713
  */
714
714
  enabled?: boolean,
715
+
716
+ /** Whether to enable endpoint collection for API Security.
717
+ * @default true
718
+ */
719
+ endpointCollectionEnabled?: boolean,
720
+
721
+ /** Maximum number of endpoints that can be serialized per message.
722
+ * @default 300
723
+ */
724
+ endpointCollectionMessageLimit?: number,
715
725
  },
716
726
  /**
717
727
  * Configuration for RASP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.63.2",
3
+ "version": "5.64.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -174,7 +174,7 @@
174
174
  "jszip": "^3.10.1",
175
175
  "mocha": "^11.6.0",
176
176
  "multer": "^2.0.2",
177
- "nock": "^11.9.1",
177
+ "nock": "^13.5.6",
178
178
  "nyc": "^15.1.0",
179
179
  "octokit": "^5.0.3",
180
180
  "proxyquire": "^1.8.0",
@@ -98,7 +98,18 @@ function wrapWithTracer (fn) {
98
98
  return function () {
99
99
  const options = arguments[0]
100
100
 
101
- options.experimental_telemetry ??= { isEnabled: true, tracer: noopTracer }
101
+ const experimentalTelemetry = options.experimental_telemetry
102
+ if (experimentalTelemetry?.isEnabled === false) {
103
+ return fn.apply(this, arguments)
104
+ }
105
+
106
+ if (experimentalTelemetry == null) {
107
+ options.experimental_telemetry = { isEnabled: true, tracer: noopTracer }
108
+ } else {
109
+ experimentalTelemetry.isEnabled = true
110
+ experimentalTelemetry.tracer ??= noopTracer
111
+ }
112
+
102
113
  wrapTracer(options.experimental_telemetry.tracer)
103
114
 
104
115
  return fn.apply(this, arguments)
@@ -166,6 +166,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
166
166
  this.knownTestsForThisSuite = hasKnownTests
167
167
  ? (knownTests?.jest?.[this.testSuite] || [])
168
168
  : this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
169
+ log.debug(`this.knownTestsForThisSuite is an array: ${Array.isArray(this.knownTestsForThisSuite)}`)
170
+ log.debug(`this.knownTestsForThisSuite is null: ${this.knownTestsForThisSuite === null}`)
171
+ log.debug(`this.knownTestsForThisSuite is undefined: ${this.knownTestsForThisSuite === undefined}`)
169
172
  } catch {
170
173
  // If there has been an error parsing the tests, we'll disable Early Flake Deteciton
171
174
  this.isEarlyFlakeDetectionEnabled = false
@@ -469,7 +472,10 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
469
472
  }
470
473
  }
471
474
  if (this.isKnownTestsEnabled) {
472
- const isNew = !this.knownTestsForThisSuite?.includes(originalTestName)
475
+ // Check if knownTestsForThisSuite is an array, since the worker may not have provided this information.
476
+ // If it's null or undefined, we can't determine if the test is new.
477
+ const isNew = Array.isArray(this.knownTestsForThisSuite) &&
478
+ !this.knownTestsForThisSuite.includes(originalTestName)
473
479
  if (isNew && !isSkipped && !retriedTestsToNumAttempts.has(originalTestName)) {
474
480
  retriedTestsToNumAttempts.set(originalTestName, 0)
475
481
  if (this.isEarlyFlakeDetectionEnabled) {
@@ -148,8 +148,8 @@ function getPlaywrightConfig (playwrightRunner) {
148
148
  }
149
149
  }
150
150
 
151
- function getRootDir (playwrightRunner) {
152
- const config = getPlaywrightConfig(playwrightRunner)
151
+ function getRootDir (playwrightRunner, configArg) {
152
+ const config = configArg?.config || getPlaywrightConfig(playwrightRunner)
153
153
  if (config.rootDir) {
154
154
  return config.rootDir
155
155
  }
@@ -162,8 +162,8 @@ function getRootDir (playwrightRunner) {
162
162
  return process.cwd()
163
163
  }
164
164
 
165
- function getProjectsFromRunner (runner) {
166
- const config = getPlaywrightConfig(runner)
165
+ function getProjectsFromRunner (runner, configArg) {
166
+ const config = configArg?.projects ? configArg : getPlaywrightConfig(runner)
167
167
  return config.projects?.map((project) => {
168
168
  if (project.project) {
169
169
  return project.project
@@ -509,11 +509,12 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
509
509
  return dispatcherExport
510
510
  }
511
511
 
512
- function runnerHook (runnerExport, playwrightVersion) {
513
- shimmer.wrap(runnerExport.Runner.prototype, 'runAllTests', runAllTests => async function () {
512
+ function runAllTestsWrapper (runAllTests, playwrightVersion) {
513
+ // Config parameter is only available from >=1.55.0
514
+ return async function (config) {
514
515
  let onDone
515
516
 
516
- rootDir = getRootDir(this)
517
+ rootDir = getRootDir(this, config)
517
518
 
518
519
  const processArgv = process.argv.slice(2).join(' ')
519
520
  const command = `playwright ${processArgv}`
@@ -586,7 +587,7 @@ function runnerHook (runnerExport, playwrightVersion) {
586
587
  }
587
588
  }
588
589
 
589
- const projects = getProjectsFromRunner(this)
590
+ const projects = getProjectsFromRunner(this, config)
590
591
 
591
592
  const shouldSetRetries = isFlakyTestRetriesEnabled &&
592
593
  flakyTestRetriesCount > 0 &&
@@ -651,6 +652,23 @@ function runnerHook (runnerExport, playwrightVersion) {
651
652
  // TODO: we can trick playwright into thinking the session passed by returning
652
653
  // 'passed' here. We might be able to use this for both EFD and Test Management tests.
653
654
  return runAllTestsReturn
655
+ }
656
+ }
657
+
658
+ function runnerHook (runnerExport, playwrightVersion) {
659
+ shimmer.wrap(
660
+ runnerExport.Runner.prototype,
661
+ 'runAllTests',
662
+ runAllTests => runAllTestsWrapper(runAllTests, playwrightVersion)
663
+ )
664
+ }
665
+
666
+ function runnerHookNew (runnerExport, playwrightVersion) {
667
+ runnerExport = shimmer.wrap(runnerExport, 'runAllTestsWithConfig', function (originalGetter) {
668
+ const originalFunction = originalGetter.call(this)
669
+ return function () {
670
+ return runAllTestsWrapper(originalFunction, playwrightVersion)
671
+ }
654
672
  })
655
673
 
656
674
  return runnerExport
@@ -694,6 +712,12 @@ addHook({
694
712
  versions: ['>=1.38.0']
695
713
  }, runnerHook)
696
714
 
715
+ addHook({
716
+ name: 'playwright',
717
+ file: 'lib/runner/testRunner.js',
718
+ versions: ['>=1.55.0']
719
+ }, runnerHookNew)
720
+
697
721
  addHook({
698
722
  name: 'playwright',
699
723
  file: 'lib/runner/dispatcher.js',
@@ -53,8 +53,8 @@ class PrismaEngine extends DatabasePlugin {
53
53
  options.resource = originalStatement
54
54
  options.type = type || engineSpan.attributes['db.system']
55
55
  options.meta['db.type'] = dbType || engineSpan.attributes['db.system']
56
- options.meta['db.instance'] = dbConfig?.database
57
- options.meta['db.name'] = dbConfig?.user
56
+ options.meta['db.name'] = dbConfig?.database
57
+ options.meta['db.user'] = dbConfig?.user
58
58
  options.meta['out.host'] = dbConfig?.host
59
59
  options.meta[CLIENT_PORT_KEY] = dbConfig?.port
60
60
  }
@@ -323,12 +323,6 @@ class Config {
323
323
  }
324
324
  }
325
325
 
326
- if (typeof options.runtimeMetrics?.gc === 'boolean') {
327
- options.runtimeMetrics.gc = {
328
- enabled: options.runtimeMetrics.gc
329
- }
330
- }
331
-
332
326
  const DD_INSTRUMENTATION_INSTALL_ID = coalesce(
333
327
  getEnvironmentVariable('DD_INSTRUMENTATION_INSTALL_ID'),
334
328
  null
@@ -493,6 +487,8 @@ class Config {
493
487
  defaults.apmTracingEnabled = true
494
488
  defaults['appsec.apiSecurity.enabled'] = true
495
489
  defaults['appsec.apiSecurity.sampleDelay'] = 30
490
+ defaults['appsec.apiSecurity.endpointCollectionEnabled'] = true
491
+ defaults['appsec.apiSecurity.endpointCollectionMessageLimit'] = 300
496
492
  defaults['appsec.blockedTemplateGraphql'] = undefined
497
493
  defaults['appsec.blockedTemplateHtml'] = undefined
498
494
  defaults['appsec.blockedTemplateJson'] = undefined
@@ -690,6 +686,8 @@ class Config {
690
686
  DD_AGENT_HOST,
691
687
  DD_API_SECURITY_ENABLED,
692
688
  DD_API_SECURITY_SAMPLE_DELAY,
689
+ DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED,
690
+ DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT,
693
691
  DD_APM_TRACING_ENABLED,
694
692
  DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE,
695
693
  DD_APPSEC_COLLECT_ALL_HEADERS,
@@ -846,6 +844,10 @@ class Config {
846
844
  ))
847
845
  this._setBoolean(env, 'appsec.apiSecurity.enabled', DD_API_SECURITY_ENABLED && isTrue(DD_API_SECURITY_ENABLED))
848
846
  env['appsec.apiSecurity.sampleDelay'] = maybeFloat(DD_API_SECURITY_SAMPLE_DELAY)
847
+ this._setBoolean(env, 'appsec.apiSecurity.endpointCollectionEnabled',
848
+ DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED)
849
+ env['appsec.apiSecurity.endpointCollectionMessageLimit'] =
850
+ maybeInt(DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT)
849
851
  env['appsec.blockedTemplateGraphql'] = maybeFile(DD_APPSEC_GRAPHQL_BLOCKED_TEMPLATE_JSON)
850
852
  env['appsec.blockedTemplateHtml'] = maybeFile(DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML)
851
853
  this._envUnprocessed['appsec.blockedTemplateHtml'] = DD_APPSEC_HTTP_BLOCKED_TEMPLATE_HTML
@@ -1073,6 +1075,10 @@ class Config {
1073
1075
  options.experimental?.appsec?.standalone && !options.experimental.appsec.standalone.enabled
1074
1076
  ))
1075
1077
  this._setBoolean(opts, 'appsec.apiSecurity.enabled', options.appsec?.apiSecurity?.enabled)
1078
+ this._setBoolean(opts, 'appsec.apiSecurity.endpointCollectionEnabled',
1079
+ options.appsec?.apiSecurity?.endpointCollectionEnabled)
1080
+ opts['appsec.apiSecurity.endpointCollectionMessageLimit'] =
1081
+ maybeInt(options.appsec?.apiSecurity?.endpointCollectionMessageLimit)
1076
1082
  opts['appsec.blockedTemplateGraphql'] = maybeFile(options.appsec?.blockedTemplateGraphql)
1077
1083
  opts['appsec.blockedTemplateHtml'] = maybeFile(options.appsec?.blockedTemplateHtml)
1078
1084
  this._optsUnprocessed['appsec.blockedTemplateHtml'] = options.appsec?.blockedTemplateHtml
@@ -1190,7 +1196,7 @@ class Config {
1190
1196
  this._setBoolean(opts, 'reportHostname', options.reportHostname)
1191
1197
  this._setBoolean(opts, 'runtimeMetrics.enabled', options.runtimeMetrics?.enabled)
1192
1198
  this._setBoolean(opts, 'runtimeMetrics.eventLoop', options.runtimeMetrics?.eventLoop)
1193
- this._setBoolean(opts, 'runtimeMetrics.gc', options.runtimeMetrics?.gc?.enabled)
1199
+ this._setBoolean(opts, 'runtimeMetrics.gc', options.runtimeMetrics?.gc)
1194
1200
  this._setBoolean(opts, 'runtimeMetricsRuntimeId', options.runtimeMetricsRuntimeId)
1195
1201
  this._setArray(opts, 'sampler.spanSamplingRules', reformatSpanSamplingRules(options.spanSamplingRules))
1196
1202
  this._setUnit(opts, 'sampleRate', coalesce(options.sampleRate, options.ingestion.sampleRate))
@@ -86,7 +86,7 @@ class DatadogTracer {
86
86
  }
87
87
 
88
88
  try {
89
- if (format !== 'text_map_dsm') {
89
+ if (format !== 'text_map_dsm' && format !== formats.LOG) {
90
90
  this._prioritySampler.sample(context)
91
91
  }
92
92
  this._propagators[format].inject(context, carrier)
@@ -274,9 +274,10 @@ function startGCObserver () {
274
274
  gcObserver = new PerformanceObserver(list => {
275
275
  for (const entry of list.getEntries()) {
276
276
  const type = gcType(entry.detail?.kind || entry.kind)
277
+ const duration = entry.duration * 1_000_000
277
278
 
278
- runtimeMetrics.histogram('runtime.node.gc.pause.by.type', entry.duration, `gc_type:${type}`)
279
- runtimeMetrics.histogram('runtime.node.gc.pause', entry.duration)
279
+ runtimeMetrics.histogram('runtime.node.gc.pause.by.type', duration, `gc_type:${type}`)
280
+ runtimeMetrics.histogram('runtime.node.gc.pause', duration)
280
281
  }
281
282
  })
282
283
 
@@ -8,6 +8,8 @@
8
8
  "DD_API_KEY": ["A"],
9
9
  "DD_API_SECURITY_ENABLED": ["A"],
10
10
  "DD_API_SECURITY_SAMPLE_DELAY": ["A"],
11
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED": ["A"],
12
+ "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT": ["A"],
11
13
  "DD_APM_FLUSH_DEADLINE_MILLISECONDS": ["A"],
12
14
  "DD_APM_TRACING_ENABLED": ["A"],
13
15
  "DD_APPSEC_AUTO_USER_INSTRUMENTATION_MODE": ["A"],
@@ -0,0 +1,124 @@
1
+ 'use strict'
2
+
3
+ const dc = require('dc-polyfill')
4
+ const { sendData } = require('./send-data')
5
+
6
+ const fastifyRouteCh = dc.channel('apm:fastify:route:added')
7
+
8
+ let config
9
+ let application
10
+ let host
11
+ let getRetryData
12
+ let updateRetryData
13
+
14
+ /**
15
+ * Keep track of endpoints that still need to be sent.
16
+ * Map key is `${METHOD} ${PATH}`, value is { method, path }
17
+ */
18
+ const pendingEndpoints = new Map()
19
+ let flushScheduled = false
20
+ let isFirstPayload = true
21
+
22
+ function endpointKey (method, path) {
23
+ return `${method.toUpperCase()} ${path}`
24
+ }
25
+
26
+ function scheduleFlush () {
27
+ if (flushScheduled) return
28
+ flushScheduled = true
29
+ setImmediate(flushAndSend).unref()
30
+ }
31
+
32
+ function recordEndpoint (method, path) {
33
+ const key = endpointKey(method, path)
34
+ if (pendingEndpoints.has(key)) return
35
+
36
+ pendingEndpoints.set(key, { method: method.toUpperCase(), path })
37
+ scheduleFlush()
38
+ }
39
+
40
+ function onFastifyRoute (routeData) {
41
+ const routeOptions = routeData?.routeOptions
42
+ if (!routeOptions?.path) return
43
+
44
+ const methods = Array.isArray(routeOptions.method) ? routeOptions.method : [routeOptions.method]
45
+
46
+ for (const method of methods) {
47
+ recordEndpoint(method, routeOptions.path)
48
+ }
49
+ }
50
+
51
+ function buildEndpointObjects (endpoints) {
52
+ return endpoints.map(({ method, path }) => {
53
+ return {
54
+ type: 'REST',
55
+ method,
56
+ path,
57
+ operation_name: 'http.request',
58
+ resource_name: endpointKey(method, path)
59
+ }
60
+ })
61
+ }
62
+
63
+ function flushAndSend () {
64
+ flushScheduled = false
65
+ if (pendingEndpoints.size === 0) return
66
+
67
+ const batchEndpoints = []
68
+ for (const [key, endpoint] of pendingEndpoints) {
69
+ batchEndpoints.push(endpoint)
70
+ pendingEndpoints.delete(key)
71
+ if (batchEndpoints.length >= config.appsec?.apiSecurity?.endpointCollectionMessageLimit) break
72
+ }
73
+
74
+ const payloadObj = {
75
+ is_first: isFirstPayload,
76
+ endpoints: buildEndpointObjects(batchEndpoints)
77
+ }
78
+
79
+ let reqType = 'app-endpoints'
80
+ let payload = payloadObj
81
+
82
+ const retryData = getRetryData()
83
+ if (retryData) {
84
+ payload = [
85
+ { request_type: 'app-endpoints', payload: payloadObj },
86
+ { request_type: retryData.reqType, payload: retryData.payload }
87
+ ]
88
+ reqType = 'message-batch'
89
+ }
90
+
91
+ sendData(config, application, host, reqType, payload, updateRetryData)
92
+
93
+ if (isFirstPayload) {
94
+ isFirstPayload = false
95
+ }
96
+
97
+ // If more endpoints accumulated while sending, schedule another flush.
98
+ if (pendingEndpoints.size) scheduleFlush()
99
+ }
100
+
101
+ function start (_config = {}, _application, _host, getRetryDataFunction, updateRetryDataFunction) {
102
+ if (!_config?.appsec?.apiSecurity?.endpointCollectionEnabled) return
103
+
104
+ config = _config
105
+ application = _application
106
+ host = _host
107
+ getRetryData = getRetryDataFunction
108
+ updateRetryData = updateRetryDataFunction
109
+
110
+ fastifyRouteCh.subscribe(onFastifyRoute)
111
+ }
112
+
113
+ function stop () {
114
+ fastifyRouteCh.unsubscribe(onFastifyRoute)
115
+
116
+ pendingEndpoints.clear()
117
+ flushScheduled = false
118
+ config = application = host = getRetryData = updateRetryData = null
119
+ }
120
+
121
+ module.exports = {
122
+ start,
123
+ stop
124
+ }
@@ -3,6 +3,7 @@ const tracerVersion = require('../../../../package.json').version
3
3
  const dc = require('dc-polyfill')
4
4
  const os = require('os')
5
5
  const dependencies = require('./dependencies')
6
+ const endpoints = require('./endpoints')
6
7
  const { sendData } = require('./send-data')
7
8
  const { errors } = require('../startup-log')
8
9
  const { manager: metricsManager } = require('./metrics')
@@ -254,6 +255,7 @@ function start (aConfig, thePluginManager) {
254
255
 
255
256
  dependencies.start(config, application, host, getRetryData, updateRetryData)
256
257
  telemetryLogger.start(config)
258
+ endpoints.start(config, application, host, getRetryData, updateRetryData)
257
259
 
258
260
  sendData(config, application, host, 'app-started', appStarted(config))
259
261
 
@@ -280,6 +282,7 @@ function stop () {
280
282
 
281
283
  telemetryStopChannel.publish(getTelemetryData())
282
284
 
285
+ endpoints.stop()
283
286
  config = undefined
284
287
  }
285
288