dd-trace 5.123.0 → 5.124.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 (97) hide show
  1. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  2. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  3. package/index.d.ts +128 -26
  4. package/package.json +10 -10
  5. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  6. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  7. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  10. package/packages/datadog-instrumentations/src/jest.js +140 -18
  11. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  12. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  13. package/packages/datadog-instrumentations/src/pino.js +15 -4
  14. package/packages/datadog-instrumentations/src/playwright.js +34 -20
  15. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
  16. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  17. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  18. package/packages/datadog-instrumentations/src/winston.js +6 -3
  19. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  20. package/packages/datadog-plugin-http/src/client.js +2 -15
  21. package/packages/datadog-plugin-http2/src/client.js +2 -15
  22. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  23. package/packages/datadog-plugin-playwright/src/index.js +9 -1
  24. package/packages/datadog-plugin-undici/src/index.js +2 -15
  25. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  26. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  27. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  28. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  29. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  30. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  31. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  32. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  33. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  35. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  36. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  37. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  38. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  39. package/packages/dd-trace/src/config/index.js +2 -1
  40. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  42. package/packages/dd-trace/src/dogstatsd.js +45 -18
  43. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  44. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  45. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  46. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  47. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  48. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  49. package/packages/dd-trace/src/flush.js +102 -0
  50. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  51. package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
  52. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  53. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  54. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  55. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  56. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  57. package/packages/dd-trace/src/llmobs/index.js +80 -17
  58. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  59. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  60. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  61. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  62. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  63. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  64. package/packages/dd-trace/src/noop/span.js +5 -0
  65. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  66. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  67. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  68. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  69. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  70. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  72. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  73. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  74. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  75. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  76. package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
  77. package/packages/dd-trace/src/opentracing/span.js +28 -0
  78. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  79. package/packages/dd-trace/src/plugin_manager.js +2 -0
  80. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  81. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  82. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  83. package/packages/dd-trace/src/profiling/oom.js +4 -3
  84. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  85. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  86. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  87. package/packages/dd-trace/src/proxy.js +36 -8
  88. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  89. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  90. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  91. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  92. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  93. package/packages/dd-trace/src/serverless.js +37 -20
  94. package/packages/dd-trace/src/span_stats.js +40 -3
  95. package/packages/dd-trace/src/tracer.js +22 -0
  96. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  97. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
@@ -26,6 +26,15 @@ const finishedRegistry = createRegistry('finished')
26
26
  let OTEL_ENABLED = false
27
27
  const ALLOWED = new Set(['string', 'number', 'boolean'])
28
28
 
29
+ /**
30
+ * @typedef {object} RecordedException
31
+ * @property {string} message
32
+ * @property {string} [name]
33
+ * @property {string} [stack]
34
+ * @typedef {string | number | boolean} SpanEventAttributeScalar
35
+ * @typedef {SpanEventAttributeScalar | string[] | number[] | boolean[]} SpanEventAttributeValue
36
+ */
37
+
29
38
  const integrationCounters = {
30
39
  spans_created: {},
31
40
  spans_finished: {},
@@ -306,6 +315,25 @@ class DatadogSpan {
306
315
  this._events.push(event)
307
316
  }
308
317
 
318
+ /**
319
+ * @param {RecordedException} exception
320
+ * @param {Record<string, SpanEventAttributeValue>} [attributes]
321
+ */
322
+ recordException (exception, attributes) {
323
+ const { message, name, stack } = exception
324
+ const eventAttributes = { ...attributes }
325
+ eventAttributes['exception.message'] ??= message
326
+
327
+ if (typeof name === 'string') {
328
+ eventAttributes['exception.type'] ??= name
329
+ }
330
+ if (typeof stack === 'string') {
331
+ eventAttributes['exception.stacktrace'] ??= stack
332
+ }
333
+
334
+ this.addEvent('exception', eventAttributes)
335
+ }
336
+
309
337
  finish (finishTime) {
310
338
  if (this._duration !== undefined) {
311
339
  return
@@ -105,6 +105,10 @@ const THREAD_ID = String(threadId)
105
105
  // time onEnter activates the span, and cleared on span finish, which
106
106
  // is how a pendingEndpoints waiter recognizes a record it must no
107
107
  // longer append to.
108
+ // webTags: the web-server tag bag the record's endpoint comes from, or is
109
+ // waiting on; undefined while the span has no web-server ancestry.
110
+ // Distinguishes a stale announcement from a live one when a nearer
111
+ // web-server span takes over.
108
112
  const CachedSym = Symbol('OtelThreadCtx.cached')
109
113
 
110
114
  let started = false
@@ -137,6 +141,7 @@ function getOrBuildContext (span) {
137
141
  span[CachedSym] = cached
138
142
  }
139
143
  cached.context = new ThreadContext(traceId, spanId, attrs)
144
+ cached.webTags = webTags
140
145
  if (endpoint === undefined) awaitEndpoint(webTags, cached)
141
146
  return cached.context
142
147
  }
@@ -227,21 +232,34 @@ function onEndpointResolved (span) {
227
232
  if (endpoint === undefined) return
228
233
  pendingEndpoints.delete(webTags)
229
234
  for (const cached of waiting) {
230
- if (cached.context !== undefined) appendEndpoint(cached.context, endpoint)
235
+ // A record whose span has since been attributed to a nearer web-server span
236
+ // enlisted again under that bag, and this one is no longer its endpoint.
237
+ if (cached.context !== undefined && cached.webTags === webTags) appendEndpoint(cached.context, endpoint)
231
238
  }
232
239
  }
233
240
 
234
- // A span whose record was built before it looked like a web-server span at all
235
- // has just been recognized as one, so it has no endpoint and never enlisted for
236
- // this request. Its ancestry can't have changed, only its own tags, so this is
237
- // only ever about the announced span's own record.
241
+ // webTagsCache has changed which request it attributes this span to: the span
242
+ // itself or an ancestor was recognized as a web-server span, giving a record
243
+ // built without any web-server ancestry its first endpoint, or a nearer
244
+ // web-server span superseded the one this record is showing. Give the record
245
+ // the new request's endpoint if it has settled, or wait for its announcement.
238
246
  function onWebTagsResolved (span) {
239
247
  if (!started) return
240
248
  const cached = span[CachedSym]
241
249
  if (cached === undefined || cached.context === undefined) return
242
250
  const webTags = webTagsCache.getCachedWebTags(span)
251
+ if (webTags === cached.webTags) return
252
+ // Recorded so that an announcement for the bag this record was previously
253
+ // waiting on no longer applies to it.
254
+ cached.webTags = webTags
243
255
  const endpoint = finalEndpoint(webTags)
244
256
  if (endpoint === undefined) {
257
+ // A record that already shows the outer request's endpoint goes on showing
258
+ // it until the nearer one settles: the record buffer is append-only, so
259
+ // there is no way to take the attribute back, and rebuilding the
260
+ // ThreadContext would strand every async-context frame holding this one.
261
+ // The work is still nested in the outer request, which makes that the least
262
+ // wrong of the values available.
245
263
  awaitEndpoint(webTags, cached)
246
264
  } else {
247
265
  appendEndpoint(cached.context, endpoint)
@@ -165,6 +165,7 @@ module.exports = class PluginManager {
165
165
  codeOriginForSpans,
166
166
  dbmPropagationMode,
167
167
  dsmEnabled,
168
+ DD_TRACE_HTTP_CLIENT_ERROR_STATUSES,
168
169
  DD_TRACE_HTTP_SERVER_ERROR_STATUSES,
169
170
  clientIpEnabled,
170
171
  clientIpHeader,
@@ -195,6 +196,7 @@ module.exports = class PluginManager {
195
196
  site,
196
197
  url,
197
198
  headers: headerTags || [],
199
+ DD_TRACE_HTTP_CLIENT_ERROR_STATUSES,
198
200
  DD_TRACE_HTTP_SERVER_ERROR_STATUSES,
199
201
  clientIpHeader,
200
202
  DD_TEST_SESSION_NAME,
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../log')
4
+
5
+ const statusCodeRangesPattern = /^[1-5]\d{2}(?:-[1-5]\d{2})?(?:,[1-5]\d{2}(?:-[1-5]\d{2})?)*$/
6
+ const whitespacePattern = /\s/g
7
+ const MAX_HTTP_STATUS_CODE = 599
8
+
9
+ const SERVER_ERROR_STATUSES = 'DD_TRACE_HTTP_SERVER_ERROR_STATUSES'
10
+ const CLIENT_ERROR_STATUSES = 'DD_TRACE_HTTP_CLIENT_ERROR_STATUSES'
11
+
12
+ /** @typedef {(code: number) => boolean} StatusValidator */
13
+
14
+ /** @type {StatusValidator} */
15
+ function isNotServerErrorCode (code) {
16
+ return code < 500
17
+ }
18
+
19
+ /** @type {StatusValidator} */
20
+ function isNotClientErrorCode (code) {
21
+ return code < 400 || code >= 500
22
+ }
23
+
24
+ /**
25
+ * @param {{ validateStatus?: unknown, DD_TRACE_HTTP_SERVER_ERROR_STATUSES?: unknown }} config
26
+ * @returns {StatusValidator}
27
+ */
28
+ function getServerStatusValidator (config) {
29
+ return getStatusValidator(config, SERVER_ERROR_STATUSES, '500-599', isNotServerErrorCode)
30
+ }
31
+
32
+ /**
33
+ * @param {{ validateStatus?: unknown, DD_TRACE_HTTP_CLIENT_ERROR_STATUSES?: unknown }} config
34
+ * @returns {StatusValidator}
35
+ */
36
+ function getClientStatusValidator (config) {
37
+ return getStatusValidator(config, CLIENT_ERROR_STATUSES, '400-499', isNotClientErrorCode)
38
+ }
39
+
40
+ /**
41
+ * @param {Record<string, unknown>} config
42
+ * @param {string} optionName - Name of the configuration holding the error status ranges.
43
+ * @param {string} defaultRanges - Ranges covered by `defaultValidator`
44
+ * @param {StatusValidator} defaultValidator
45
+ * @returns {StatusValidator}
46
+ */
47
+ function getStatusValidator (config, optionName, defaultRanges, defaultValidator) {
48
+ if (typeof config.validateStatus === 'function') {
49
+ return /** @type {StatusValidator} */ (config.validateStatus)
50
+ } else if (Object.hasOwn(config, 'validateStatus')) {
51
+ log.error('Expected `validateStatus` to be a function.')
52
+ }
53
+
54
+ const ranges = config[optionName]
55
+ if (ranges === undefined) return defaultValidator
56
+ if (typeof ranges !== 'string') {
57
+ log.error('Expected `%s` to be a string.', optionName)
58
+ return defaultValidator
59
+ }
60
+ if (ranges === defaultRanges) return defaultValidator
61
+
62
+ const normalized = ranges.replaceAll(whitespacePattern, '')
63
+ if (normalized === defaultRanges) return defaultValidator
64
+ if (!statusCodeRangesPattern.test(normalized)) {
65
+ log.error('`%s` must contain comma-separated status codes or ranges from 100 to 599.', optionName)
66
+ return defaultValidator
67
+ }
68
+
69
+ const errorStatusCodes = new Uint8Array(MAX_HTTP_STATUS_CODE + 1)
70
+ for (const range of normalized.split(',')) {
71
+ const separator = range.indexOf('-')
72
+ if (separator === -1) {
73
+ errorStatusCodes[Number(range)] = 1
74
+ continue
75
+ }
76
+
77
+ const first = Number(range.slice(0, separator))
78
+ const second = Number(range.slice(separator + 1))
79
+ const start = Math.min(first, second)
80
+ const end = Math.max(first, second)
81
+ errorStatusCodes.fill(1, start, end + 1)
82
+ }
83
+
84
+ /**
85
+ * @param {number} code
86
+ * @returns {boolean}
87
+ */
88
+ function isValidStatusCode (code) {
89
+ return errorStatusCodes[code] !== 1
90
+ }
91
+
92
+ return isValidStatusCode
93
+ }
94
+
95
+ module.exports = {
96
+ getClientStatusValidator,
97
+ getServerStatusValidator,
98
+ }
@@ -541,6 +541,7 @@ module.exports = {
541
541
  getRelativeCoverageFiles,
542
542
  getLineCoverageBitmap,
543
543
  applySkippedCoverageToCoverage,
544
+ getTestCoverageLinesData,
544
545
  getTestCoverageLinesPercentage,
545
546
  resetCoverage,
546
547
  mergeCoverage,
@@ -1359,6 +1360,34 @@ function getLineCoverageBitmap (lineCoverage, onlyCoveredLines = false) {
1359
1360
  return bitmap
1360
1361
  }
1361
1362
 
1363
+ function getLineCoverageBitmaps (lineCoverage) {
1364
+ let maxLine = 0
1365
+ const entries = Object.entries(lineCoverage)
1366
+
1367
+ for (const [line] of entries) {
1368
+ const lineNumber = Number(line)
1369
+ if (Number.isSafeInteger(lineNumber) && lineNumber > maxLine) {
1370
+ maxLine = lineNumber
1371
+ }
1372
+ }
1373
+ if (maxLine === 0) return {}
1374
+
1375
+ const length = Math.ceil((maxLine + 1) / 8)
1376
+ const coveredBitmap = Buffer.alloc(length)
1377
+ const executableBitmap = Buffer.alloc(length)
1378
+ for (const [line, hits] of entries) {
1379
+ const lineNumber = Number(line)
1380
+ if (!Number.isSafeInteger(lineNumber) || lineNumber <= 0) continue
1381
+
1382
+ const byteIndex = lineNumber >> 3
1383
+ const bit = 1 << (lineNumber % 8)
1384
+ executableBitmap[byteIndex] |= bit
1385
+ if (hits) coveredBitmap[byteIndex] |= bit
1386
+ }
1387
+
1388
+ return { coveredBitmap, executableBitmap }
1389
+ }
1390
+
1362
1391
  function mergeCoverageBitmaps (targetBitmap, bitmap) {
1363
1392
  if (!targetBitmap) {
1364
1393
  return Buffer.from(bitmap)
@@ -1419,16 +1448,6 @@ function getCoverageFileBitmap (bitmap) {
1419
1448
  }
1420
1449
  }
1421
1450
 
1422
- function addCoverageFilesToMap (files, targetMap, rootDir) {
1423
- for (const file of files) {
1424
- const bitmap = getCoverageFileBitmap(file.bitmap)
1425
- if (!bitmap) continue
1426
-
1427
- const filename = rootDir ? getTestSuitePath(file.filename, rootDir) : file.filename
1428
- targetMap.set(filename, mergeCoverageBitmaps(targetMap.get(filename), bitmap))
1429
- }
1430
- }
1431
-
1432
1451
  function addSkippedCoverageToMap (skippedCoverage, targetMap) {
1433
1452
  if (!skippedCoverage) return
1434
1453
 
@@ -1439,23 +1458,57 @@ function addSkippedCoverageToMap (skippedCoverage, targetMap) {
1439
1458
  }
1440
1459
  }
1441
1460
 
1442
- function getTestCoverageLinesPercentage (coverage, skippedCoverage, rootDir) {
1443
- const executableLinesByFile = new Map()
1444
- const coveredLinesByFile = new Map()
1461
+ /**
1462
+ * Calculates line coverage and optionally returns executable-line coverage files in the same traversal.
1463
+ * @param {object} coverage
1464
+ * @param {object} [skippedCoverage]
1465
+ * @param {string} [rootDir]
1466
+ * @param {boolean} [includeExecutableFiles]
1467
+ * @returns {{ percentage: number, executableFiles?: Array<{ filename: string, bitmap: Buffer }> }}
1468
+ */
1469
+ function getTestCoverageLinesData (coverage, skippedCoverage, rootDir, includeExecutableFiles = false) {
1470
+ const coverageMap = getCoverageMap(coverage)
1471
+ const skippedCoverageByFilename = getSkippedCoverageByFilename(skippedCoverage)
1472
+ const coverageByFilename = new Map()
1473
+ const executableFiles = includeExecutableFiles ? [] : undefined
1474
+
1475
+ for (const filename of coverageMap.files()) {
1476
+ const fileCoverage = coverageMap.fileCoverageFor(filename)
1477
+ const { coveredBitmap, executableBitmap } = getLineCoverageBitmaps(fileCoverage.getLineCoverage())
1478
+ if (!executableBitmap) continue
1445
1479
 
1446
- addCoverageFilesToMap(getExecutableFilesFromCoverage(coverage), executableLinesByFile, rootDir)
1447
- addCoverageFilesToMap(getCoveredFilesFromCoverage(coverage), coveredLinesByFile, rootDir)
1448
- addSkippedCoverageToMap(skippedCoverage, coveredLinesByFile)
1480
+ const relativeFilename = rootDir ? getTestSuitePath(filename, rootDir) : filename
1481
+ const existingCoverage = coverageByFilename.get(relativeFilename)
1482
+ if (existingCoverage) {
1483
+ existingCoverage.coveredBitmap = mergeCoverageBitmaps(existingCoverage.coveredBitmap, coveredBitmap)
1484
+ existingCoverage.executableBitmap = mergeCoverageBitmaps(existingCoverage.executableBitmap, executableBitmap)
1485
+ } else {
1486
+ coverageByFilename.set(relativeFilename, { coveredBitmap, executableBitmap })
1487
+ }
1488
+ }
1449
1489
 
1450
1490
  let totalExecutableLines = 0
1451
1491
  let totalCoveredLines = 0
1452
-
1453
- for (const [filename, executableLines] of executableLinesByFile) {
1454
- totalExecutableLines += countBitmapBits(executableLines)
1455
- totalCoveredLines += countCoveredExecutableBits(coveredLinesByFile.get(filename), executableLines)
1492
+ for (const [filename, { coveredBitmap, executableBitmap }] of coverageByFilename) {
1493
+ const skippedBitmap = skippedCoverageByFilename.get(filename)
1494
+ const combinedCoveredBitmap = skippedBitmap
1495
+ ? mergeCoverageBitmaps(coveredBitmap, skippedBitmap)
1496
+ : coveredBitmap
1497
+ totalExecutableLines += countBitmapBits(executableBitmap)
1498
+ totalCoveredLines += countCoveredExecutableBits(combinedCoveredBitmap, executableBitmap)
1499
+ if (executableFiles) {
1500
+ executableFiles.push({ filename, bitmap: executableBitmap })
1501
+ }
1456
1502
  }
1457
1503
 
1458
- return totalExecutableLines === 0 ? 0 : Math.floor((totalCoveredLines / totalExecutableLines) * 10_000) / 100
1504
+ const percentage = totalExecutableLines === 0
1505
+ ? 0
1506
+ : Math.floor((totalCoveredLines / totalExecutableLines) * 10_000) / 100
1507
+ return { percentage, executableFiles }
1508
+ }
1509
+
1510
+ function getTestCoverageLinesPercentage (coverage, skippedCoverage, rootDir) {
1511
+ return getTestCoverageLinesData(coverage, skippedCoverage, rootDir).percentage
1459
1512
  }
1460
1513
 
1461
1514
  function isLineCoveredByBitmap (bitmap, line) {
@@ -13,6 +13,7 @@ const { storage } = require('../../../../datadog-core')
13
13
  const legacyStorage = storage('legacy')
14
14
  const urlFilter = require('./urlfilter')
15
15
  const { createInferredProxySpan, finishInferredProxySpan } = require('./inferred_proxy')
16
+ const { getServerStatusValidator } = require('./status-validator')
16
17
  const { extractURL, obfuscateQs, getQsObfuscator, calculateHttpEndpoint } = require('./url')
17
18
  const { NETWORK_PEER_ADDRESS } = require('./http-otel-semantics')
18
19
 
@@ -35,11 +36,6 @@ const MANUAL_DROP = tags.MANUAL_DROP
35
36
 
36
37
  const contexts = new WeakMap()
37
38
  const requests = new WeakMap()
38
- const statusCodeRangesPattern = /^[1-5]\d{2}(?:-[1-5]\d{2})?(?:,[1-5]\d{2}(?:-[1-5]\d{2})?)*$/
39
- const whitespacePattern = /\s/g
40
- const MAX_HTTP_STATUS_CODE = 599
41
-
42
- /** @typedef {(code: number) => boolean} StatusValidator */
43
39
 
44
40
  // TODO: change this to no longer rely on creating a dummy plugin to be able to access startSpan
45
41
  function createWebPlugin (tracer, config = {}) {
@@ -65,7 +61,7 @@ const web = {
65
61
  // Ensure the configuration has the correct structure and defaults.
66
62
  normalizeConfig (config) {
67
63
  const headers = getHeadersToRecord(config)
68
- const validateStatus = getStatusValidator(config)
64
+ const validateStatus = getServerStatusValidator(config)
69
65
  const hooks = getHooks(config)
70
66
  const filter = urlFilter.getFilter(config)
71
67
  const middleware = getMiddlewareSetting(config)
@@ -563,65 +559,6 @@ function getHeadersToRecord (config) {
563
559
  return []
564
560
  }
565
561
 
566
- function isNot500ErrorCode (code) {
567
- return code < 500
568
- }
569
-
570
- /**
571
- * @param {{ validateStatus?: unknown, DD_TRACE_HTTP_SERVER_ERROR_STATUSES?: unknown }} config
572
- * @returns {StatusValidator}
573
- */
574
- function getStatusValidator (config) {
575
- if (typeof config.validateStatus === 'function') {
576
- return /** @type {StatusValidator} */ (config.validateStatus)
577
- } else if (config.hasOwnProperty('validateStatus')) {
578
- log.error('Expected `validateStatus` to be a function.')
579
- }
580
-
581
- const { DD_TRACE_HTTP_SERVER_ERROR_STATUSES } = config
582
- if (DD_TRACE_HTTP_SERVER_ERROR_STATUSES === undefined) return isNot500ErrorCode
583
- if (typeof DD_TRACE_HTTP_SERVER_ERROR_STATUSES !== 'string') {
584
- log.error('Expected `DD_TRACE_HTTP_SERVER_ERROR_STATUSES` to be a string.')
585
- return isNot500ErrorCode
586
- }
587
- if (DD_TRACE_HTTP_SERVER_ERROR_STATUSES === '500-599') return isNot500ErrorCode
588
-
589
- const normalized = DD_TRACE_HTTP_SERVER_ERROR_STATUSES.replaceAll(whitespacePattern, '')
590
- if (normalized === '500-599') return isNot500ErrorCode
591
- if (!statusCodeRangesPattern.test(normalized)) {
592
- log.error(
593
- '`DD_TRACE_HTTP_SERVER_ERROR_STATUSES` must contain comma-separated status codes or ranges from 100 to 599.'
594
- )
595
- return isNot500ErrorCode
596
- }
597
-
598
- const errorStatusCodes = new Uint8Array(MAX_HTTP_STATUS_CODE + 1)
599
- const ranges = normalized.split(',')
600
- for (const range of ranges) {
601
- const separator = range.indexOf('-')
602
- if (separator === -1) {
603
- errorStatusCodes[Number(range)] = 1
604
- continue
605
- }
606
-
607
- const first = Number(range.slice(0, separator))
608
- const second = Number(range.slice(separator + 1))
609
- const start = Math.min(first, second)
610
- const end = Math.max(first, second)
611
- errorStatusCodes.fill(1, start, end + 1)
612
- }
613
-
614
- /**
615
- * @param {number} code
616
- * @returns {boolean}
617
- */
618
- function isValidStatusCode (code) {
619
- return errorStatusCodes[code] !== 1
620
- }
621
-
622
- return isValidStatusCode
623
- }
624
-
625
562
  const noop = () => {}
626
563
 
627
564
  function getHooks (config) {
@@ -56,16 +56,17 @@ function strategiesToCallbackMode (strategies, callbackMode) {
56
56
  function buildExportCommand (exporters, tags) {
57
57
  const tagString = [...Object.entries(tags),
58
58
  ['snapshot', snapshotKinds.ON_OUT_OF_MEMORY]].map(([key, value]) => `${key}:${value}`).join(',')
59
- const urls = []
59
+ let urls = ''
60
60
  for (const exporter of exporters) {
61
61
  const url = exporter.getExportUrl()
62
62
  if (url !== undefined) {
63
- urls.push(url.toString())
63
+ if (urls) urls += ','
64
+ urls += url.toString()
64
65
  }
65
66
  }
66
67
  return [process.execPath,
67
68
  path.join(__dirname, 'exporter_cli.js'),
68
- urls.join(','), tagString, 'space']
69
+ urls, tagString, 'space']
69
70
  }
70
71
 
71
72
  module.exports = { oomExportStrategies, ensureOOMExportStrategies, strategiesToCallbackMode, buildExportCommand }
@@ -95,6 +95,10 @@ class Profiler extends EventEmitter {
95
95
  for (const key of keys) {
96
96
  this.#customLabelKeys.add(key)
97
97
  }
98
+ this.#applyCustomLabelKeys()
99
+ }
100
+
101
+ #applyCustomLabelKeys () {
98
102
  if (this.#profilers) {
99
103
  for (const profiler of this.#profilers) {
100
104
  profiler.setCustomLabelKeys?.(this.#customLabelKeys)
@@ -180,6 +184,9 @@ class Profiler extends EventEmitter {
180
184
  this.#profilers = profilers
181
185
  this.#uploadCompression = uploadCompression
182
186
  this.#systemInfoReport = systemInfoReport
187
+ if (this.#customLabelKeys.size > 0) {
188
+ this.#applyCustomLabelKeys()
189
+ }
183
190
 
184
191
  this._setInterval()
185
192
  // Log errors if the source map finder fails, but don't prevent the rest
@@ -124,13 +124,13 @@ class GCDecorator {
124
124
  }
125
125
  let reasonLabel = this.reasonLabels[flags]
126
126
  if (!reasonLabel) {
127
- const reasons = []
127
+ let reasonStr = ''
128
128
  for (const [key, value] of Object.entries(this.flagObj)) {
129
129
  if (value & flags) {
130
- reasons.push(key)
130
+ if (reasonStr) reasonStr += ','
131
+ reasonStr += key
131
132
  }
132
133
  }
133
- const reasonStr = reasons.join(',')
134
134
  reasonLabel = labelFromStr(this.stringTable, this.reasonLabelKey, reasonStr)
135
135
  this.reasonLabels[flags] = reasonLabel
136
136
  }
@@ -138,7 +138,7 @@ class NativeWallProfiler {
138
138
 
139
139
  this.#pprof.time.start({
140
140
  collectCpuTime: this.#cpuProfilingEnabled,
141
- columnNumbers: 'pack',
141
+ columnNumbers: 'emit',
142
142
  durationMillis: this.#flushIntervalMillis,
143
143
  intervalMicros: this.#samplingIntervalMicros,
144
144
  lineNumbers: false,
@@ -168,8 +168,8 @@ class NativeWallProfiler {
168
168
  spanFinishCh.subscribe(this.#boundSpanFinished)
169
169
  if (this.#endpointCollectionEnabled) {
170
170
  // Web-tags cache publishes once per span at the moment its
171
- // walk-result transitions from undefined to a real value
172
- // exactly when we need to refresh the ProfilingContext snapshot.
171
+ // walk-result changes exactly when we need to refresh the
172
+ // ProfilingContext snapshot.
173
173
  webTagsCache.resolvedCh.subscribe(this.#boundSpanTagsUpdated)
174
174
  // Turn on the cache's own tagsUpdate subscription — it's
175
175
  // ref-counted, so this composes with any other active consumer.
@@ -229,13 +229,12 @@ class NativeWallProfiler {
229
229
  } else if (current !== sampleContext) {
230
230
  this.#pprof.time.setContext(sampleContext)
231
231
  }
232
- // Every setContext() call in ACF mode allocates a fresh contextHolder
233
- // (a node::ObjectWrap with its own v8::Global<v8::Value>) in the native
234
- // profiler. Skip the call if the CPED already holds this sampleContext,
235
- // which is the common case when the same span is repeatedly activated:
236
- // #getProfilingContext caches profilingContext on span[ProfilingContext],
237
- // so identity comparison short-circuits.
238
232
  } else if (current !== sampleContext) {
233
+ // Every setContext() call in ACF mode allocates a fresh contextHolder
234
+ // (a node::ObjectWrap with its own v8::Global<v8::Value>) in the native
235
+ // profiler. We're only incurring the cost when current !== sampleContext
236
+ // and skip it when current === sampleContext, which is the common case
237
+ // when the same span is repeatedly activated.
239
238
  this.#pprof.time.setContext(sampleContext)
240
239
  }
241
240
  } else {
@@ -266,8 +265,8 @@ class NativeWallProfiler {
266
265
  }
267
266
 
268
267
  // webTags is snapshotted into the sample context at getProfilingContext
269
- // time; if the answer turns out to be undefined and the span later gets
270
- // web-server tags, #spanTagsUpdated refreshes this field via the shared
268
+ // time; if the answer changes later the span or an ancestor becomes a
269
+ // web-server span #spanTagsUpdated refreshes this field via the shared
271
270
  // cache (see web-tags-cache.js).
272
271
  const webTags = this.#endpointCollectionEnabled ? webTagsCache.getCachedWebTags(span) : undefined
273
272
 
@@ -291,9 +290,10 @@ class NativeWallProfiler {
291
290
  }
292
291
 
293
292
  // Invoked (via webTagsCache.resolvedCh) once per span at the moment the
294
- // shared cache promotes a previously-undefined webTags answer into a
295
- // real value. Refresh the ProfilingContext snapshot so future samples
296
- // pick it up.
293
+ // shared cache changes its webTags answer: an empty one promoted into a real
294
+ // value, or an outer request's tag bag replaced by a nearer one. Refresh the
295
+ // ProfilingContext snapshot in place so samples already holding it — including
296
+ // those of a span that stays active across the promotion — pick it up.
297
297
  #spanTagsUpdated (span) {
298
298
  if (!this.#started) return
299
299
  const profilingContext = span[ProfilingContext]